
简介这是一份基于 C# Windows 与 SQL Server 的三层架构宿舍信息管理系统完整源码面向课程设计、毕业设计或初步学习 WinForm 与数据库开发的读者。系统实现管理员登录注册、用户信息修改以及学生宿舍信息的添加、删除、按学号姓名精确查询、按名字模糊查询和修改等核心功能代码按 UI、BLL、DAL、Models 分层组织清晰展示三层架构实际落位各层职责清楚便于按需改造和学习。zip 压缩包共 126 个文件整体约 495KB包含 C# 源码.cs、项目文件.csproj、DLL 引用、PDB 调试符号、resx/resources 资源文件以及配置与说明文本结构轻量方便直接打开调试。目前已有 155 人学习适合作为课设参考或三层架构入门练手项目附有百度网盘演示视频链接可对照界面操作查看登录、增删查改等流程帮助读者快速掌握 WinForm 结合 SQL Server 的常见开发方法。1. 宿舍信息管理系统三层架构版到底比单文件强在哪C# Windows 课设和毕设里宿舍信息管理系统是被点名的常客。但绝大多数人的第一版代码是把所有 SQL 和界面逻辑全堆进 Form1.cs跑是能跑就是后期改一个字段名要翻几百行代码。这套资源不一样的地方在于它用的是典型的 UI BLL DAL 三层架构数据库落在 SQL Server 上功能覆盖用户登录注册、学生宿舍信息的增删查改适合两类人——正在做课设想找个规范参考的以及想快速抄一份能演示、能答辩的 C# 管理系统源码的从业者。文章后面会直接讲清楚每一层怎么拆、表怎么建、SQL 怎么写还有那些资源包里看起来像报错的缓存文件到底是什么。2. 三层架构的项目结构UI/BLL/DAL 拆到什么程度才算合格2.1 为什么必须拆三层职责边界与“玄学”的真相很多刚入门 C# 的人觉得三层架构是形式主义甚至管它叫玄学——代码跑起来都一样为什么要分成三个项目但真正接手过管理系统的人会告诉你拆三层的核心原因是改需求的时候不用全量返工。UI 层winfrom只负责窗体展示和收集用户输入它不知道数据存在哪个数据库、表名叫什么也没有任何关系。BLL 层winfrom.BLL承载业务规则比如退宿操作要先把宿舍余量加一再删除学生记录这类跨表逻辑必须放在这里。DAL 层winfrom.DAL只做最纯粹的数据操作——增、删、查、改一个方法对应一条 SQL。Models 层则是三个层的信使用户在界面上填的内容先装进实体对象再一层一层往下传。直接说结论当你发现改一个字段要从 DataGridView 找到 SQL 语句时说明单文件的写法已经到极限了。而三层架构里字段改动的路径是 UI 改控件名 → Models 改属性 → DAL 改 SQL 参数每一处都有明确的位置不会出现明明改了数据库却不知道界面数据从哪来的情况。2.2 资源包里的文件与项目骨架谁参与编译谁只是缓存打开这套资源包第一眼看到的是winfrom.csprojAssemblyReference.cache、DesignTimeResolveAssemblyReferencesInput.cache这一堆文件很多新手第一反应是项目是不是坏了。这里可以明确告诉你这些文件全是 Visual Studio 编译过程中生成的二进制缓存不是源码也不参与最终生成 EXE删掉它们对项目没有任何影响。真正决定项目结构的是下面这几个文件文件/目录作用是否需要拷进源码包winfrom.csproj界面层项目文件是winfrom.BLL.csproj业务层项目文件是winfrom.DAL.csproj数据层项目文件是winfrom.Models.csproj实体层项目文件是App.config存放数据库连接字符串是*.cs各层源代码是*.cacheVS 编译缓存否可无视BLL、DAL、Models 各建一个类库项目UI 层引用它们三个这是标准做法。DAL 引用 ModelsBLL 引用 DAL 和 ModelsUI 引用 BLL 和 Models方向不能反——如果 DAL 去引用 UI那就循环依赖了VS 直接编译不过。App.config 里的连接字符串是整个系统的命脉写法如下?xml version1.0 encodingutf-8 ? configuration connectionStrings add nameDormitoryDB connectionStringData Source.;Initial CatalogDormitoryDB;Integrated SecurityTrue providerNameSystem.Data.SqlClient / /connectionStrings /configuration这段配置在连接字符串有两个关键点Data Source.表示连接本机默认 SQL Server 实例如果你装了命名实例比如SQLEXPRESS这里要写成.\SQLEXPRESS或者localhost\SQLEXPRESSIntegrated SecurityTrue走的是 Windows 身份验证不需要写用户名密码本地调试最省事。如果目标机器必须用 SQL Server 账号登录改成Data Source.;Initial CatalogDormitoryDB;User IDsa;Password你的密码。2.3 实体层 Models三个类对应三张业务表的写法实体类的作用是在三层之间搬运数据。这套系统里核心实体有两个用户实体和宿舍学生信息实体。namespace winfrom.Models { /// summary /// 管理员用户实体 /// /summary public class SysUser { public int Id { get; set; } // 用户ID主键自增 public string UserName { get; set; } // 登录账号 public string Password { get; set; } // 登录密码 public string RealName { get; set; } // 真实姓名 public string Phone { get; set; } // 联系电话 } /// summary /// 学生宿舍信息实体 /// /summary public class StudentInfo { public int Id { get; set; } // 自增主键 public string StudentNo { get; set; } // 学号精确查询用 public string StudentName { get; set; } // 姓名模糊查询用 public string Gender { get; set; } // 性别 public int BuildingNo { get; set; } // 楼栋号如 3 号楼 public int RoomNo { get; set; } // 宿舍号如 501 public string Phone { get; set; } // 联系电话 public string Remark { get; set; } // 备注信息 } }这里有个容易被忽略的约定属性名要和数据库字段名一一对应不能搞出StudentName对应数据库name这种映射虽然可以写 SQL 别名解决但完全没有必要。学号用string而不是int是因为学号经常以 0 开头或者包含字母int会丢前导零——这条是实际开发里踩过的坑后面还有详细说明。实体类里不要放业务方法它就是一个纯数据容器。如果你发现某个实体类里开始出现计算年龄之类的方法说明该把业务逻辑挪到 BLL 层了。3. 数据库设计与连接字符串先把表建对再写代码3.1 建库建表宿舍信息表和用户表的核心字段设计数据库设计决定了后面所有代码的写法。这套系统的表结构并不复杂两张表就够SysUser存管理员账号StudentInfo存宿舍学生信息。直接在 SQL Server Management Studio 里执行下面的脚本CREATE DATABASE DormitoryDB; GO USE DormitoryDB; GO -- 管理员表 CREATE TABLE SysUser ( Id INT IDENTITY(1,1) PRIMARY KEY, UserName NVARCHAR(50) NOT NULL UNIQUE, Password NVARCHAR(50) NOT NULL, RealName NVARCHAR(50) NULL, Phone NVARCHAR(20) NULL ); -- 学生宿舍信息表 CREATE TABLE StudentInfo ( Id INT IDENTITY(1,1) PRIMARY KEY, StudentNo NVARCHAR(20) NOT NULL UNIQUE, StudentName NVARCHAR(50) NOT NULL, Gender NVARCHAR(2) NULL, BuildingNo INT NULL, RoomNo INT NULL, Phone NVARCHAR(20) NULL, Remark NVARCHAR(200) NULL );字段类型有两个地方要专门说明。第一账号、学号、姓名全部用NVARCHAR而不是VARCHAR因为NVARCHAR能正确存储中文避免出现中文乱码这种经典问题。第二UserName加了UNIQUE约束注册新账号时如果重名数据库直接报错这比先在代码里查一遍再插入要可靠得多——并发情况下代码查重会漏。性别字段用NVARCHAR(2)而不是CHAR(1)纯粹是为了省事避免 C# 端还要做类型转换。真正需要关注的是BuildingNo和RoomNo用INT这两个字段将来会参与按楼栋查询宿舍空位之类的统计整数比字符串更适合做条件运算。3.2 连接字符串本机 SQL Server 最稳的写法连接字符串写不对项目连编译都过不了更谈不上跑起来。最常见的是下面两种写法验证方式连接字符串适用场景Windows 验证Data Source.;Initial CatalogDormitoryDB;Integrated SecurityTrue本机调试、课设演示SQL Server 验证Data Source.;Initial CatalogDormitoryDB;User IDsa;Password123456远程部署、服务器环境如果你用的是第二种而 SQL Server 只开了 Windows 身份验证模式那么运行时会报用户 sa 登录失败。解决方法是打开 SQL Server Management Studio右键服务器 → 属性 → 安全性 → 选中SQL Server 和 Windows 身份验证模式然后重启 SQL Server 服务。这个操作不做后面所有连接都是白搭属于典型的配置五分钟排错两小时。在 DAL 层读取连接字符串统一用ConfigurationManager不要在每个方法里硬编码字符串using System.Configuration; using System.Data; using System.Data.SqlClient; namespace winfrom.DAL { public class DBHelper { private static readonly string ConnStr ConfigurationManager.ConnectionStrings[DormitoryDB].ConnectionString; /// summary /// 获取一个已打开的数据库连接 /// /summary public static SqlConnection GetConnection() { SqlConnection conn new SqlConnection(ConnStr); conn.Open(); return conn; } } }GetConnection()里的conn.Open()是必需的很多新手把连接对象传给SqlCommand之后才 Open一旦执行顺序错了就报连接未打开或已关闭。把 Open 封装进这个静态方法里调用方只需要关心SqlConnection用完之后 Dispose不用每次都在窗体代码里写一遍 Open。3.3 登录验证怎么写ExecuteReader 该做哪些检查用户登录是这套系统读操作里最典型的场景。DAL 层的方法只负责执行查询并返回结果不负责弹窗提示登录成功还是失败。public bool Login(string userName, string password) { string sql SELECT COUNT(*) FROM SysUser WHERE UserName u AND Password p; using (SqlConnection conn DBHelper.GetConnection()) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(u, userName); cmd.Parameters.AddWithValue(p, password); return (int)cmd.ExecuteScalar() 0; } }这个方法里值得说的有三处。第一using语句保证连接和命令对象在方法结束随手释放这是 C# 操作 SQL Server 的标准姿势避免连接泄漏把数据库耗死。第二ExecuteScalar()用于返回单个值COUNT(*)的结果是int所以前面要强转不转的话(int)会直接抛异常。第三条件查询用u和p这种参数占位符而不是字符串拼接原因下一章专门讲。这里有一个容易被忽略的业务判断登录验证应该只查是否存在匹配账号密码至于该账号已被禁用这种状态判断应该放在 BLL 层再加一个条件去查。把业务规则和数据访问混在一个方法里是三层架构最常见的污染。4. 增删查改完整链路从 DAL 到 UI 一次跑通4.1 添加宿舍信息ExecuteNonQuery 与参数化 SQL对管理系统来说增删查改四个字是灵魂。先看添加功能它在 DAL 层的长相是这样的public bool InsertStudent(Models.StudentInfo stu) { string sql INSERT INTO StudentInfo(StudentNo, StudentName, Gender, BuildingNo, RoomNo, Phone, Remark) VALUES(StudentNo, StudentName, Gender, BuildingNo, RoomNo, Phone, Remark); using (SqlConnection conn DBHelper.GetConnection()) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(StudentNo, stu.StudentNo); cmd.Parameters.AddWithValue(StudentName, stu.StudentName); cmd.Parameters.AddWithValue(Gender, stu.Gender); cmd.Parameters.AddWithValue(BuildingNo, stu.BuildingNo); cmd.Parameters.AddWithValue(RoomNo, stu.RoomNo); cmd.Parameters.AddWithValue(Phone, stu.Phone); cmd.Parameters.AddWithValue(Remark, stu.Remark); return cmd.ExecuteNonQuery() 0; } }参数化 SQL 是这段代码的核心价值。用AddWithValue把所有来自界面的值都包装成参数好处有两个一是避免 SQL 注入用户如果在输入框里填; DROP TABLE StudentInfo;--这种恶意内容拼接 SQL 直接就会执行破坏语句参数化之后它只是一个普通的字符串值二是不再需要人工处理引号不用写stu.StudentName这种既丑又容易出错的东西。另外注意ExecuteNonQuery()的返回值它表示受影响的行数。插入一条成功返回 1失败返回 0。UI 层拿到这个返回值后判断 1再弹出添加成功的提示。很多人在这个位置只写ExecuteNonQuery()不判断返回值结果就是数据库里有没有写进去界面完全不知道。BLL 层的对应方法极薄只是透传和补业务规则public bool AddStudent(Models.StudentInfo stu) { if (string.IsNullOrWhiteSpace(stu.StudentNo) || string.IsNullOrWhiteSpace(stu.StudentName)) throw new Exception(学号和姓名不能为空); return new DAL.StudentDAL().InsertStudent(stu); }参数校验放在 BLL 层而不是 UI 层是为了防止有人绕过界面直接调接口。BLL 层校验通过后再调 DAL这样职责链就是完整的。4.2 查询精确查询与按姓名模糊查询的两种实现查询功能是这套系统里最常被调用的方法。精确查询按学号定位返回单条记录模糊查询按姓名匹配返回多条记录。两种查询的 DAL 写法差别只在 SQL 的 WHERE 子句public DataTable GetStudentByNo(string studentNo) { string sql SELECT * FROM StudentInfo WHERE StudentNo no; using (SqlConnection conn DBHelper.GetConnection()) using (SqlDataAdapter da new SqlDataAdapter(sql, conn)) { da.SelectCommand.Parameters.AddWithValue(no, studentNo); DataTable dt new DataTable(); da.Fill(dt); return dt; } } public DataTable GetStudentByName(string name) { string sql SELECT * FROM StudentInfo WHERE StudentName LIKE name; using (SqlConnection conn DBHelper.GetConnection()) using (SqlDataAdapter da new SqlDataAdapter(sql, conn)) { da.SelectCommand.Parameters.AddWithValue(name, % name %); DataTable dt new DataTable(); da.Fill(dt); return dt; } }注意模糊查询这里有个关键细节参数值是% name %而不是在 SQL 语句里写LIKE %name%。如果把通配符写进 SQL 字符串参数化的name就失去意义SQL 会把它当成一个字面量去匹配结果就是查不到任何数据。这是 C# 里做模糊查询时翻车率最高的写法之一。SqlDataAdapter配DataTable是查询多条记录的经典组合。Fill()之后连接会自动关闭省去手动 Close 的麻烦。UI 层调用时直接把返回的DataTable绑到DataGridView.DataSource上即可显示。4.3 修改UPDATE 必须带主键条件修改功能是最容易出大事故的地方。看这段代码它几乎代表了这类系统的标准姿势public bool UpdateStudent(Models.StudentInfo stu) { string sql UPDATE StudentInfo SET StudentNamename, Gendergender, BuildingNobuilding, RoomNoroom, Phonephone, Remarkremark WHERE Idid; using (SqlConnection conn DBHelper.GetConnection()) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(name, stu.StudentName); cmd.Parameters.AddWithValue(gender, stu.Gender); cmd.Parameters.AddWithValue(building, stu.BuildingNo); cmd.Parameters.AddWithValue(room, stu.RoomNo); cmd.Parameters.AddWithValue(phone, stu.Phone); cmd.Parameters.AddWithValue(remark, stu.Remark); cmd.Parameters.AddWithValue(id, stu.Id); return cmd.ExecuteNonQuery() 0; } }关键在于WHERE Idid这个条件。如果漏写或者条件写成WHERE StudentNono而界面传来的学号恰好为空那么这条 UPDATE 会作用于全表所有宿舍记录都会被改成同一份数据。这类事故在课设答辩现场出现过不止一次属于一票否决的级别。Id从哪来修改界面上通常有一列专门保存选中行的主键值要么存到隐藏列里要么存到一个成员变量里。最稳妥的做法是在DataGridView的CellClick事件里把当前行的Id存下来。4.4 删除影响行数为 0 时的业务处理删除功能除了调DELETE语句还要处理好删不到数据的情况public bool DeleteStudent(int id) { string sql DELETE FROM StudentInfo WHERE Idid; using (SqlConnection conn DBHelper.GetConnection()) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(id, id); return cmd.ExecuteNonQuery() 0; } }这段代码有个实际业务场景值得说明点击删除时如果选中的记录已经被人先删掉了ExecuteNonQuery()返回 0此时界面的提示应该是未找到可删除的记录而不是删除失败。两者听上去差不多但对用户来说含义完全不同。BLL 层可以加一层判断区分这两种情况避免 UI 层拿到 false 后只能弹出含糊的提示。UI 层删除前的MessageBox.Show(确定删除吗, 提示, MessageBoxButtons.YesNo)属于交互习惯生硬地把确认弹窗写进 BLL 层就是错误设计——业务层不该依赖 Windows 窗体控件。5. 避坑排查五个让系统跑不起来的真实案例5.1 连接层翻车连接不上 SQL Server 的经典套路排在第一位的坑是建立到服务器的连接时发生错误。现象程序一启动就报错或者点击登录按钮时抛出SqlException。原因通常是两个一是 SQL Server 服务没启动二是连接字符串用的验证模式和数据库实际配置不一致比如代码里写sa账号但数据库只开了 Windows 身份验证。解决先到服务里确认 SQL Server 服务状态为正在运行再按第 3.2 节的两种验证方式对照检查。如果本机装了多个实例确认Data Source指向的是你建库的那个实例SQL Server 的默认实例名经常被忽略。第二个高发型问题同样在连接层就是超时时间已到或连接池已满。现象系统运行一段时间后所有数据库操作都卡死或报超时。原因代码里SqlConnection没有释放每次操作都占着一个连接连接池耗尽之后新的连接只能排队等超时。解决所有 DAL 方法的连接创建统一走using或者用DBHelper.GetConnection()里那种封装方式确保Dispose一定被调用——这条对任何基于 ADO.NET 的 C# 项目都适用尤其是将来要扩展成上位机或对外开放接口时连接泄漏会直接拖垮整个程序。5.2 数据层翻车类型、空值和 SQL 语义第三个坑是学号变成了科学计数法。现象DataGridView 里明明存的是2019001显示出来却变成2.01901E06或者其他奇怪的格式。原因读取时用了SET ANSI_PADDING OFF或者 C# 端把StudentNo属性设成了int类型——数字类型的默认格式化会把前导零丢掉123456 这种还会被转成科学计数法。解决学号、楼栋号这些看起来像数字但不是数字的字段一律用NVARCHAR存、string读不要在 Models 层给它定义成int。这个问题的隐蔽程度很高因为单条记录看不太出来一旦在 DataGridView 里展示多行格式就乱了。第四个坑是整个系统里最隐蔽的玄学——输出了结果但界面上不刷新。现象执行完添加操作数据库里明明多了记录但 DataGridView 列表还是老的。原因查询结果绑定的DataTable在内存里缓存了没有重新查询。解决每次添加、修改、删除成功后重新调用一次查询方法把新的DataTable重新赋给DataSource。这类问题不加调试手段很难定位建议在绑定 DataSource 后调用dataGridView.Refresh()并且把绑定的数据源重新赋值而不是只Refresh()控件。第五个坑来自资源包本身。现象打开项目后发现一堆.cache文件担心源码损坏或缺少关键文件。原因那些是 Visual Studio 的编译中间产物不是项目文件的一部分。解决拷贝源码、上传网盘或用 Git 管理时手动把*.cache和obj、bin目录排除掉源码包里只留.sln、.csproj、.cs、.config和.sql文件。如果从网上下载的压缩包里有缓存文件直接删除即可完全不影响编译。6. 进阶收尾参数化查询、密码哈希与退宿事务的三个护身符三层的架子搭好、增删查改能跑之后还有三件事值得做。第一件是把系统里所有拼接 SQL 的地方彻底换成参数化查询包括模糊查询在内不留任何 value 的字符串拼接这是防止 SQL 注入的底线。第二件是密码不要明文存。当前SysUser表里Password字段是明文演示没问题但如果想把这份代码写进简历或实际部署建议在 BLL 层加一个 SHA256 哈希再入库。做法是在注册时把密码哈希后存入数据库登录时把用户输入的密码同样哈希后去比对。public static string HashPassword(string raw) { using (var sha System.Security.Cryptography.SHA256.Create()) { byte[] bytes System.Text.Encoding.UTF8.GetBytes(raw); byte[] hash sha.ComputeHash(bytes); return Convert.ToBase64String(hash); } }第三件是处理退宿这个业务动作时用事务保证两步操作不脱节。退宿不只是删掉学生记录还要把对应宿舍的余量字段加一如果第一步删除成功第二步更新失败宿舍余量就错乱了。using (SqlConnection conn DBHelper.GetConnection()) { SqlTransaction tx conn.BeginTransaction(); try { // 第一步删除学生记录 // 第二步UPDATE 宿舍余量 1 tx.Commit(); } catch { tx.Rollback(); throw; } }从那以后我每次写这类管理系统都会强制自己把连接放 using、SQL 走参数化、跨表操作用事务这三件事走一遍不为别的就是不想答辩现场出事故。这套资源本身已经把三层架构和基本增删查改都实现了你拿到手先跑通再按这三个方向加固就比绝大多数课设版本完整。希望帮到你。本文还有配套的精品资源点击获取