尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C#宿舍管理系统开发实战:表结构设计、WinForms实现与避坑指南

C#宿舍管理系统开发实战:表结构设计、WinForms实现与避坑指南 简介一份面向C#课程设计场景的宿舍管理系统完整源码包以Visual Studio项目为主体配套文档、流程图与SQL数据库脚本适用于需要完成同类课程设计或进行WinForm开发练习的初学者。系统按学生与宿管双角色设计覆盖公告发布、宿舍信息增删改查、物品报修审批、请假销假、用户登录管理等基础业务界面借助CSkin组件美化并通过WebService获取地理位置和天气信息整体结构清晰。压缩包共188个文件约4.7MB以cs源码、resx界面资源、dll依赖库、配置文件及mdf/ldf数据库文件为主同时含docx说明书与sql脚本便于直接还原项目环境。已有1377人下载学习可作为课程设计参考、答辩演示或二次开发基础帮助理解分层调用、XML用户记录与WinForm交互等关键实现。1. 这套 C# 宿舍管理系统到底能做什么、适合谁接手宿舍管理系统是 C# 入门到进阶最经典的落地项目之一甚至可以说很多人的第一个「能给别人演示的完整系统」就是它。它的本质是一个典型的管理信息系统MIS前台是 WinForms 窗体后台是一张 SQL Server 或 SQLite 数据库表中间是增删改查。你拿到一个名为「C#宿舍管理系统.rar」的压缩包说明交付形态已经是打包好的源码工程里面大概率包含 .sln 解决方案、若干窗体文件.cs / .Designer.cs、数据库脚本和说明文档。这类系统的目标用户是宿管员和辅导员要解决的是入住登记、退宿调宿、床位统计、报修跟踪这些真实场景而不是教你写算法。这个方向对三类人最有用正在做课程设计或毕业设计的学生需要一个能跑通、能讲清楚的设计思路小团队里被要求「一周内给后勤做个宿舍管理小工具」的开发者以及想系统练一遍 C# SQL Server 数据访问、委托事件、界面绑定的自学者。下面我按自己做过类似项目的顺序拆开讲先定数据表再搭工程最后落功能、填坑、打包交付。2. 拆开 .rar 后先建这 5 张表宿舍管理系统的数据库设计2.1 核心表结构与字段设计打开 .rar 后先不要急着点 .sln 跑起来第一件事是看数据库脚本。很多翻车现场都出在表结构不合理学生和宿舍直接绑死、没有入住记录表、报修信息只存一个备注字段。我一般会建议至少建下面这 5 张表学生表、宿舍表、入住记录表、报修表、用户表。宿舍和学生不直接外键关联而是通过「入住记录」这张关系表把入住、退宿、调宿的历史串起来这是整个系统的核心设计。表结构大致是这样学生表Student学号、姓名、性别、班级、电话、状态在住/离校、当前宿舍 ID、当前床位号。状态字段很重要退宿后不能删记录只能改状态。宿舍表Dormitory楼号、房间号、床位总数、已住人数、宿管员、备注。已住人数是冗余字段但查询统计时非常快维护靠事务保证一致。入住记录表CheckRecord学生学号、宿舍 ID、床位号、入住时间、退宿时间、状态。一次调宿就是一条退宿记录加一条新入住记录。报修表Repair宿舍 ID、报修人、联系电话、故障描述、状态、提交时间、处理时间。这是最容易被人忽略但实际使用频率最高的表。用户表SysUser用户名、密码哈希、盐值、角色。系统要区分宿管员和普通管理员权限不同。从业务角度看宿舍和床位是「资源」学生是「人」入住记录是「资源和人的关系」。你设计的表一旦能把这三个层次分开后面的入住、退宿、调宿代码都会非常顺。2.2 用 SQL 脚本一次建好库和测试数据常见的做法是在 SQL Server Management Studio 里先建一个空库然后执行下面的脚本。我习惯把脚本分成三部分建库、建表、插测试数据这样重跑时不会因为顺序问题报错。-- 创建数据库如果存在则跳过 IF DB_ID(Ndormitory_db) IS NULL CREATE DATABASE dormitory_db; GO USE dormitory_db; GO -- 宿舍表 CREATE TABLE Dormitory ( DormitoryId INT IDENTITY(1,1) PRIMARY KEY, BuildingNo NVARCHAR(10) NOT NULL, -- 楼号例如 A RoomNo NVARCHAR(10) NOT NULL, -- 房间号例如 301 BedCount INT NOT NULL CHECK (BedCount 0), -- 总床位数 UsedCount INT NOT NULL DEFAULT 0, -- 已住人数 Manager NVARCHAR(20) NULL, -- 宿管员 Remark NVARCHAR(200) NULL ); GO -- 学生表 CREATE TABLE Student ( StudentNo NVARCHAR(20) PRIMARY KEY, -- 学号 Name NVARCHAR(20) NOT NULL, Gender CHAR(1) NOT NULL DEFAULT 男, ClassName NVARCHAR(50) NULL, -- 班级 Phone NVARCHAR(11) NULL, Status INT NOT NULL DEFAULT 1, -- 1在住 0离校 DormitoryId INT NULL, -- 当前宿舍 BedNo INT NULL, -- 当前床位 FOREIGN KEY (DormitoryId) REFERENCES Dormitory(DormitoryId) ); GO -- 入住记录表 CREATE TABLE CheckRecord ( RecordId INT IDENTITY(1,1) PRIMARY KEY, StudentNo NVARCHAR(20) NOT NULL, DormitoryId INT NOT NULL, BedNo INT NOT NULL, CheckInDate DATETIME NOT NULL DEFAULT GETDATE(), CheckOutDate DATETIME NULL, Status INT NOT NULL DEFAULT 1, -- 1在住 0已退宿 FOREIGN KEY (StudentNo) REFERENCES Student(StudentNo), FOREIGN KEY (DormitoryId) REFERENCES Dormitory(DormitoryId) ); GO -- 报修表 CREATE TABLE Repair ( RepairId INT IDENTITY(1,1) PRIMARY KEY, DormitoryId INT NOT NULL, Reporter NVARCHAR(20) NOT NULL, Phone NVARCHAR(11) NULL, Description NVARCHAR(500) NOT NULL, Status INT NOT NULL DEFAULT 0, -- 0待处理 1处理中 2已完成 CreateTime DATETIME NOT NULL DEFAULT GETDATE(), HandleTime DATETIME NULL, FOREIGN KEY (DormitoryId) REFERENCES Dormitory(DormitoryId) ); GO -- 用户表 CREATE TABLE SysUser ( UserId INT IDENTITY(1,1) PRIMARY KEY, UserName NVARCHAR(20) NOT NULL UNIQUE, PasswordHash NVARCHAR(64) NOT NULL, Salt NVARCHAR(16) NOT NULL, Role NVARCHAR(10) NOT NULL DEFAULT user -- admin / user ); GO建表时重点看两处Student 表里 DormitoryId 和 BedNo 是「当前状态」字段CheckRecord 才是「历史事实」。查询「某学生现在住哪」直接看 Student 表查询「某房间住过哪些人」必须看 CheckRecord。这两套数据并存是刻意的不是冗余冗余到失控而是用空间换查询复杂度。测试数据不要插太多够演示三种状态就行一个房间住满、一个房间有空床、一个学生已经离校但历史记录还在。INSERT INTO Dormitory (BuildingNo, RoomNo, BedCount, UsedCount, Manager) VALUES (A, 301, 4, 2, N张阿姨), (B, 502, 6, 0, N李大爷); GO INSERT INTO Student (StudentNo, Name, Gender, ClassName, Phone, Status, DormitoryId, BedNo) VALUES (2021001, N王小明, N男, N计算机2101, 13800000001, 1, 1, 1), (2021002, N刘洋, N男, N计算机2101, 13800000002, 1, 1, 2), (2021003, N陈静, N女, N软件2102, 13800000003, 0, NULL, NULL); GO INSERT INTO CheckRecord (StudentNo, DormitoryId, BedNo, CheckInDate, Status) VALUES (2021001, 1, 1, 2024-09-01, 1), (2021002, 1, 2, 2024-09-01, 1), (2021003, 2, 1, 2024-09-01, 0); GO一个容易忽略的参数是字符串前的 N。SQL Server 里如果往 NVARCHAR 列插入中文字符串而不带 N 前缀某些排序规则下会出现乱码或问号。这是老生常谈但几乎每届做宿舍管理系统的同学都会踩一次所以脚本里我全部写成 N中文。2.3 外键与索引查询慢和删不掉的坑从这里来宿舍管理系统的数据量基本不会超过几万条索引不是性能瓶颈真正坑人的是外键导致的删除失败和统计错误。最常见的报错是「已住的宿舍无法删除」——你明明写了 DELETE FROM Dormitory WHERE DormitoryId1但 SQL Server 报外键冲突。原因就是 Student 表和 CheckRecord 表都引用了 DormitoryId。这里的处理原则是宿舍和学生属于基础数据只允许「逻辑删除」加一个 IsDeleted 字段或修改状态不允许物理删除入住记录一旦生成永远不删只做退宿标记。这样设计之后删除操作几乎从业务代码里消失你也就不用跟外键约束死磕了。索引方面我给这三个查询建了索引CheckRecord 上的 StudentNo查某人的住宿历史、CheckRecord 上的 DormitoryId查某房间的入住记录、Repair 上的 Status统计待处理工单。建索引的 SQL 很短但注意不要在 StudentNo 这种已经是主键的列上重复建主键本身自带索引。CREATE INDEX IX_CheckRecord_StudentNo ON CheckRecord(StudentNo); CREATE INDEX IX_CheckRecord_DormitoryId ON CheckRecord(DormitoryId); CREATE INDEX IX_Repair_Status ON Repair(Status); GO索引不是越多越好。宿舍管理这种量级三五个索引足够多了反而拖慢插入速度。如果后期发现按班级统计住校人数很频繁可以在 Student.ClassName 上加一个非聚集索引这就是全部了。3. 从零搭起 C# WinForms 项目登录、主界面与数据访问层3.1 项目结构与 NuGet 包选择数据库定了之后C# 这边的工程结构我推荐拆成三层UI 层WinForms 窗体、数据访问层一个 DbHelper 类 实体类、业务层可选。对于宿舍管理系统业务逻辑不算复杂很多人直接把 SQL 写在按钮事件里也能跑但后期改起来非常痛苦尤其是换数据库或加校验时你会想骂自己。如果你拿到手的 .rar 里只有一个 Form1.cs 加一堆按钮我建议你花半小时重构成下面这个结构DormitorySystem.sln ├── DormitorySystem.UI # WinForms 项目 │ ├── Forms │ │ ├── LoginForm.cs │ │ ├── MainForm.cs │ │ ├── StudentForm.cs │ │ └── RepairForm.cs │ └── Program.cs ├── DormitorySystem.Data # 类库项目 │ ├── DbHelper.cs │ ├── Models │ │ ├── Student.cs │ │ ├── Dormitory.cs │ │ └── Repair.cs │ └── Repositories │ ├── StudentRepository.cs │ └── DormitoryRepository.cs └── DormitorySystem.slnNuGet 包只装两个System.Data.SqlClient连 SQL Server 用和 Dapper可选用来把查询结果映射成对象。我实际做的时候更喜欢用 Dapper因为它能把 DataTable 那一堆样板代码压缩掉 60%而且性能在小型系统里完全不敏感。如果你们学校要求必须手写 ADO.NET那你就只看下面手写版本逻辑是一样的。3.2 用 DbHelper 封装数据库访问连接字符串与参数化查询DbHelper 是这个小系统的地基。所有数据库操作都走它统一管连接字符串、打开关闭连接、参数化查询。下面的代码是基于 ADO.NET 原生写的不依赖第三方库using System; using System.Data; using System.Data.SqlClient; namespace DormitorySystem.Data { public class DbHelper { // 连接字符串统一放 App.config不要硬编码在代码里 private static readonly string _connStr System.Configuration.ConfigurationManager.ConnectionStrings[DormitoryDb].ConnectionString; // 执行增删改返回受影响行数 public static int ExecuteNonQuery(string sql, SqlParameter[] parameters null) { using (var conn new SqlConnection(_connStr)) { conn.Open(); using (var cmd new SqlCommand(sql, conn)) { if (parameters ! null) cmd.Parameters.AddRange(parameters); return cmd.ExecuteNonQuery(); } } } // 执行查询返回首行首列用于统计数量 public static object ExecuteScalar(string sql, SqlParameter[] parameters null) { using (var conn new SqlConnection(_connStr)) { conn.Open(); using (var cmd new SqlCommand(sql, conn)) { if (parameters ! null) cmd.Parameters.AddRange(parameters); return cmd.ExecuteScalar(); } } } // 执行查询返回 DataTable配合 DataGridView 绑定 public static DataTable ExecuteDataTable(string sql, SqlParameter[] parameters null) { using (var conn new SqlConnection(_connStr)) { var dt new DataTable(); using (var cmd new SqlCommand(sql, conn)) { if (parameters ! null) cmd.Parameters.AddRange(parameters); using (var adapter new SqlDataAdapter(cmd)) { adapter.Fill(dt); } } return dt; } } } }这段代码里最关键的是 using 关键字。SqlConnection 和 SqlCommand 都实现了 IDisposable用 using 包住可以保证连接用完即关避免连接池耗尽。很多 C# 初学者的程序跑半天就报「连接池已满」多半是 new SqlConnection 之后没有 Dispose。连接字符串放在 App.config 里这是你拿到 .rar 后第一个要改的位置connectionStrings add nameDormitoryDb connectionStringServer.\SQLEXPRESS;Databasedormitory_db;User Idsa;Password123456;EncryptFalse;TrustServerCertificateTrue; providerNameSystem.Data.SqlClient / /connectionStrings注意 EncryptFalse 和 TrustServerCertificateTrue 这两项。新版 Microsoft.Data.SqlClient 驱动默认开启加密连接如果你本机 SQL Server 没配证书不加这两项会直接报证书链错误。这个坑在 2020 年之后出现的频率非常高。3.3 登录窗体与权限控制哈希加盐与用户状态登录功能看起来简单但我不建议直接用明文密码比对。常见的做法是注册时把密码用 SHA256 加盐后存进 SysUser 表登录时取出该用户的 Salt把输入密码重新哈希后与 PasswordHash 比对。using System; using System.Security.Cryptography; using System.Text; using System.Data.SqlClient; public static class PasswordHelper { // 生成随机盐 public static string GenerateSalt() { using (var rng RandomNumberGenerator.Create()) { byte[] bytes new byte[8]; rng.GetBytes(bytes); return Convert.ToHexString(bytes); } } // 加盐哈希 public static string HashPassword(string password, string salt) { using (var sha SHA256.Create()) { byte[] data sha.ComputeHash(Encoding.UTF8.GetBytes(password salt)); return Convert.ToHexString(data); } } } public static class LoginService { public static bool ValidateLogin(string userName, string password, out string role) { role null; string sql SELECT PasswordHash, Salt, Role FROM SysUser WHERE UserName UserName; using (var conn new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings[DormitoryDb].ConnectionString)) { conn.Open(); using (var cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(UserName, userName); using (var reader cmd.ExecuteReader()) { if (!reader.Read()) return false; string hash reader[PasswordHash].ToString(); string salt reader[Salt].ToString(); role reader[Role].ToString(); return PasswordHelper.HashPassword(password, salt) hash; } } } } }登录成功后我一般会在一个静态类 UserSession 里保存当前用户信息后续窗体通过它判断权限。public static class UserSession { public static string UserName { get; set; } public static string Role { get; set; } public static bool IsAdmin Role admin; }主窗体根据 IsAdmin 决定是否显示「用户管理」按钮。权限控制做到菜单级就够了宿舍管理系统不需要按钮级权限否则代码复杂度会上升一个量级。4. 入住、退宿与报修DataGridView 绑定与增删改查落地4.1 学生入住登记事务里同时写入住记录和更新床位状态入住登记是整个系统里最容易出 bug 的地方因为一次操作涉及三处数据Student 表要写当前宿舍和床位、CheckRecord 表要插入一条入住记录、Dormitory 表的 UsedCount 要加一。任何一个步骤失败数据就对不上了。所以必须放在一个事务里。用 C# 的 SqlTransaction 实现核心是同一个连接上先 BeginTransaction再把事务对象赋给每一个 SqlCommandpublic bool CheckIn(string studentNo, int dormitoryId, int bedNo) { string sqlCheckBed SELECT UsedCount, BedCount FROM Dormitory WHERE DormitoryId DormitoryId; string sqlUpdateDorm UPDATE Dormitory SET UsedCount UsedCount 1 WHERE DormitoryId DormitoryId AND UsedCount BedCount; string sqlUpdateStudent UPDATE Student SET DormitoryId DormitoryId, BedNo BedNo, Status 1 WHERE StudentNo StudentNo; string sqlInsertRecord INSERT INTO CheckRecord (StudentNo, DormitoryId, BedNo, CheckInDate, Status) VALUES (StudentNo, DormitoryId, BedNo, GETDATE(), 1); using (var conn new SqlConnection(_connStr)) { conn.Open(); using (var tx conn.BeginTransaction()) { try { // 第一步检查床位是否可用 using (var cmdCheck new SqlCommand(sqlCheckBed, conn, tx)) { cmdCheck.Parameters.AddWithValue(DormitoryId, dormitoryId); using (var reader cmdCheck.ExecuteReader()) { if (!reader.Read()) throw new Exception(宿舍不存在); int used Convert.ToInt32(reader[UsedCount]); int total Convert.ToInt32(reader[BedCount]); if (used total) throw new Exception(该宿舍已住满); } } // 第二步更新宿舍已住人数注意更新的 WHERE 条件里带 UsedCount BedCount 做双重校验 using (var cmdDorm new SqlCommand(sqlUpdateDorm, conn, tx)) { cmdDorm.Parameters.AddWithValue(DormitoryId, dormitoryId); if (cmdDorm.ExecuteNonQuery() 0) throw new Exception(床位已被占用); } // 第三步更新学生状态 using (var cmdStudent new SqlCommand(sqlUpdateStudent, conn, tx)) { cmdStudent.Parameters.AddWithValue(StudentNo, studentNo); cmdStudent.Parameters.AddWithValue(DormitoryId, dormitoryId); cmdStudent.Parameters.AddWithValue(BedNo, bedNo); if (cmdStudent.ExecuteNonQuery() 0) throw new Exception(学生不存在或已离校); } // 第四步插入入住记录 using (var cmdRecord new SqlCommand(sqlInsertRecord, conn, tx)) { cmdRecord.Parameters.AddWithValue(StudentNo, studentNo); cmdRecord.Parameters.AddWithValue(DormitoryId, dormitoryId); cmdRecord.Parameters.AddWithValue(BedNo, bedNo); cmdRecord.ExecuteNonQuery(); } tx.Commit(); return true; } catch { tx.Rollback(); throw; } } } }这段代码要注意一个逻辑先 SELECT 检查再 UPDATE 更新。但 SELECT 和 UPDATE 之间如果有另一个用户同时操作就会出并发问题。所以 UPDATE 语句里带了 UsedCount BedCount 条件如果影响行数是 0说明这个宿舍在你检查之后已经被别人住满了事务回滚。这是用乐观锁的方式处理床位竞争比单纯靠 SELECT 的结果判断可靠得多。4.2 退宿与调宿先查再改避免床位数据错乱退宿和入住是对称操作。退宿时要做的事CheckRecord 里把当前在住记录的 CheckOutDate 设为当前时间、Status 改为 0Student 表把 DormitoryId 和 BedNo 置空、Status 改为 0Dormitory 表 UsedCount 减一。调宿更特殊它是「一次退宿 一次入住」的组合。常见错误是分开两个事务做中间如果程序崩溃学生可能处于「旧宿舍退了、新宿舍没住进」的中间状态。我的做法是把调宿也放进一个事务先退旧宿舍再进新宿舍public bool TransferDorm(string studentNo, int newDormitoryId, int newBedNo) { // 先查出学生当前住的宿舍 string sqlQueryCurrent SELECT DormitoryId FROM Student WHERE StudentNo StudentNo AND Status 1; string sqlCheckOut UPDATE CheckRecord SET CheckOutDate GETDATE(), Status 0 WHERE StudentNo StudentNo AND Status 1; string sqlDecDorm UPDATE Dormitory SET UsedCount UsedCount - 1 WHERE DormitoryId DormitoryId; string sqlIncDorm UPDATE Dormitory SET UsedCount UsedCount 1 WHERE DormitoryId DormitoryId AND UsedCount BedCount; string sqlUpdateStudent UPDATE Student SET DormitoryId NewDormitoryId, BedNo NewBedNo WHERE StudentNo StudentNo; string sqlInsertRecord INSERT INTO CheckRecord (StudentNo, DormitoryId, BedNo, CheckInDate, Status) VALUES (StudentNo, NewDormitoryId, NewBedNo, GETDATE(), 1); // 事务写法与 CheckIn 相同不完整贴出关键点是所有 SqlCommand 都传入同一个 SqlTransaction 对象 ... }调宿的事务里要特别关注执行顺序先退旧Dormitory 减一再进新Dormitory 加一。如果把加一放在减一前面在极端情况下旧宿舍和新宿舍是同一个时会把 UsedCount 算错。虽然正常业务不会把学生调到原宿舍但代码写健壮一点没坏处。4.3 报修工单列表DataGridView 数据绑定与刷新报修功能是宿舍管理系统里最「面向真实使用」的模块因为宿管员几乎每天都要看「还有几个待处理工单」。界面很简单一个 DataGridView 显示工单列表一个按钮把选中的工单标记为已完成。用 DataGridView 绑定数据最常见的姿势是 DataTableprivate void LoadRepairList() { string sql SELECT r.RepairId, d.BuildingNo - d.RoomNo AS Room, r.Reporter, r.Phone, r.Description, r.CreateTime, CASE r.Status WHEN 0 THEN N待处理 WHEN 1 THEN N处理中 ELSE N已完成 END AS StatusText FROM Repair r JOIN Dormitory d ON r.DormitoryId d.DormitoryId ORDER BY r.CreateTime DESC; DataTable dt DbHelper.ExecuteDataTable(sql); dgvRepair.DataSource dt; // 隐藏不友好的列只保留展示列 dgvRepair.Columns[RepairId].Visible false; dgvRepair.Columns[Description].Width 250; }这里有个参数设置坑如果 SELECT 语句里用了别名 StatusTextDataGridView 会自动生成对应列。但如果你原先绑定过其它 DataSource再次绑定时一定要先置空再赋值否则界面会残留旧列dgvRepair.DataSource null; dgvRepair.Columns.Clear(); dgvRepair.DataSource dt;报修工单的列表刷新我建议用一个 BackgroundWorker 或 async Task 包起来因为宿管员的机器通常配置不高如果工单多了直接在 UI 线程查数据库会让窗体卡死。C# 里最简单的是用 async/await 配合 Task.Runprivate async void btnRefresh_Click(object sender, EventArgs e) { btnRefresh.Enabled false; try { DataTable dt await Task.Run(() DbHelper.ExecuteDataTable(repairSql)); dgvRepair.DataSource null; dgvRepair.Columns.Clear(); dgvRepair.DataSource dt; } catch (Exception ex) { MessageBox.Show(加载失败 ex.Message); } finally { btnRefresh.Enabled true; } }如果你拿到手的源码里用的是 C# 老式委托 BackgroundWorker也别急着重构它一样能跑。但新项目用 async/await 更简洁还不会踩 BackgroundWorker 的 RunWorkerCompleted 事件里反复判断 e.Cancelled 的麻烦。报修状态更新本身是单条 UPDATE不需要事务直接调用 DbHelper.ExecuteNonQuery 就行。5. 宿舍管理的避坑清单连接串、中文乱码、并发改床位5.1 连接字符串报错LocalDB 和 SQLExpress 的区分现象程序一启动登录窗体还没弹出来就报「建立到服务器的连接时发生错误」或「找不到指定的 SqlServer 实例」。原因连接字符串里写的是 Server.\SQLEXPRESS但你的电脑装的是 SQL Server LocalDB实例名是 (localdb)\MSSQLLocalDB也可能是开发者版默认实例直接写 Serverlocalhost 就能连。这个错误几乎每个人都会遇到一次因为 .rar 里带的连接串是作者机器上的配置。解决先用 SQL Server Management Studio 连一次本机数据库看实例名到底是什么。或者干脆用代码里最不容易错的写法Serverlocalhost;Databasedormitory_db;Integrated SecurityTrue。如果你是 Windows 登录模式根本不用写用户名密码。如果是混合模式确认 sa 密码正确且 SQL Server 服务开启了 TCP/IP 协议。5.2 DataGridView 刷新后看不到新数据BindingSource 与列缓存现象新增一条学生记录后重新查询并绑定 DataGridView但界面里看不到新数据或者出现两套重叠的列。原因直接给 DataSource 赋了新的 DataTable但 DataGridView 的列集合还保留着旧结构而且如果之前绑过带外键的实体列表新 DataTable 的列和旧列顺序不一致数据会错位显示。解决绑定前先完全清理旧数据标准三步是dgv.DataSource null; dgv.Columns.Clear(); dgv.DataSource newDataTable。如果你要在界面上做筛选用 BindingSource 包一层这样 TextBox 里输入关键字时只需要设 bSource.Filter Name LIKE % keyword %不用每次重新查数据库。5.3 床位并发超卖只靠代码里 if 判断必翻车现象两个宿管员同时在系统里给同一个宿舍安排入住系统显示已住人数是 2但实际上安排了 3 个人宿舍表 UsedCount 变成了 3但只有 2 个床位。原因代码里是先 SELECT UsedCount BedCount满足条件再 UPDATE。两个客户端同时完成了 SELECT都认为有空床然后都执行 UPDATE结果就超卖。单独靠 C# 代码里的 if 判断挡不住并发因为检查与更新之间有时间窗口。解决用 UPDATE 语句的条件判断替代代码里的 if也就是我前面写入住登记时的做法UPDATE Dormitory SET UsedCount UsedCount 1 WHERE DormitoryId DormitoryId AND UsedCount BedCount。如果 ExecuteNonQuery 返回值是 0说明床位已被抢走直接回滚并提示。不要把 UPDATE 和 SELECT 的顺序反过来先 UPDATE 后 SELECT 无法回滚已修改的数据。这个方案不需要 SQL Server 的锁提示UPDLOCK 太复杂小型系统用乐观锁足够。5.4 导出 Excel 报错不要用 Office COM用 NPOI现象想给宿管员导出「住宿名单.xls」网上找的代码用了 Microsoft.Office.Interop.Excel结果用户机器上没装 Office或者装了 WPS运行时报「检索 COM 类工厂中 CLSID 为 {00024500-0000-0000-C000-000000000046} 的组件时失败」。原因Office COM 方式需要本机安装 Office 且版本兼容部署时这个问题无解你不能要求所有宿管员的电脑都装正版 Office。解决NuGet 装 NPOI它是纯托管代码的 Excel 读写库不依赖 Office。核心代码是把 DataTable 逐行写入 HSSFWorkbook然后 SaveAs 到文件。注意文件后缀用 .xls 对应 HSSF 格式用 .xlsx 要换成 XSSFWorkbook两者 API 略有不同。导出时如果数据量大放到 BackgroundWorker 里做避免界面假死。5.5 中文乱码排序规则和文件编码双重背锅现象SQL Server 里查出来的中文正常但程序界面上显示问号或者反过来程序里正常写进数据库变成乱码。原因两个层面。第一连接字符串里没加 CharacterSet 相关配置其实 SQL Server 的 NVARCHAR 不依赖这个真正常见的是建表时列类型用了 VARCHAR 而不是 NVARCHAR导致中文按 ASCII 截断。第二C# 源文件本身编码不是 UTF-8导致字符串字面量里的中文在编译后变成乱码。解决所有存储用户输入的列统一用 NVARCHAR这是第一原则。第二在 Visual Studio 里把源码文件另存为 UTF-8 带 BOM 格式避免中文注释乱码。第三如果已经乱码先查数据库里实际存的值SELECT StudentNo, Name FROM Student。数据库里正常就说明是界面显示问题检查窗体的 Font 是否支持中文数据库里就是问号说明写入时已经错了只能重新录入。5.6 部署到别的电脑SQL Server 版本兼容现象在自己电脑上跑得好好的把发布后的 exe 拷到宿管员电脑上一启动就报「程序集版本冲突」或「此版本的 SQL Server 不支持」。原因目标机器上没有安装对应版本的 SQL Server或者你用了高级特性如 STRING_AGG、窗口函数但目标数据库版本太老。宿舍管理系统基本都是 SQL Server 2012 以上但宿管办公室的机器可能是很多年前的 SQL Server 2008 R2。解决交付前把数据库脚本用低版本兼容语法重写一遍避免用 STRING_AGG、FORMAT、TRIM 这类新函数。连接字符串里不要写死实例名部署文档里写清楚数据库服务器地址怎么改。最保险的做法是把数据库文件.mdf连同发布 exe 一起打包附上一键附加数据库的 SQL 脚本CREATE DATABASE ... FOR ATTACH。6. 打包 .rar 之前要改好的配置与验证清单6.1 启动时自动检测数据库并初始化宿舍管理系统交付后对方大概率不会用 SSMS 手动执行建库脚本。常见做法是程序启动时先连接一次如果失败就弹窗提示并自动执行建库 SQL。实现方式是在 Program.cs 的 Main 方法里加一个初始化步骤[STAThread] static void Main() { bool dbReady DatabaseInitializer.CheckOrCreateDatabase(); if (!dbReady) { MessageBox.Show(数据库初始化失败请检查连接字符串配置或 SQL Server 服务状态。, 系统提示); return; } Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new LoginForm()); }DatabaseInitializer 里先尝试打开连接捕获 SqlException 后执行嵌入的建库脚本。这种做法对宿管员非常友好他只需要改 App.config 里的服务器地址其他全自动。但注意自动建库脚本必须用事务包住避免执行到一半失败留下残缺表。6.2 交付前验证清单我每次交付这类系统都会按固定清单过一遍避免在客户现场翻车换一台干净机器只装 .NET Framework 和 SQL Server Express跑通登录、入住、退宿、报修全流程。验证日期格式中文系统下 SQL Server 默认语言可能不同所有日期字段统一用 DateTime 类型不要拼字符串。检查 App.config 里有没有写死绝对路径比如日志文件路径、Excel 导出路径全部改成相对路径或用户目录。用 NPOI 导出一次住宿名单确认文件能正常打开。连按两次入住按钮确认床位不会重复分配。最后一条特别重要有些用户会手欠双击按钮如果你的按钮事件没做防重入处理就会插入两条重复的入住记录。处理方式很简单入住成功后刷新列表把入住按钮的 Enabled 置为 false直到用户重新选择学生。6.3 .rar 压缩包里的目录组织交付给别人的 .rar 压缩包我建议按这个结构整理一个「使用说明.docx」放最前面里面写清数据库实例名、连接串怎么改、默认管理员账号密码然后是「数据库脚本」文件夹放建库 SQL 和测试数据 SQL最后是源码文件夹和发布文件夹分开不要把 obj/bin 目录里的中间文件也打包进去那会白白让压缩包膨胀几十兆。如果给的是源代码交付记得删掉 .vs 隐藏文件夹和每个项目里的 bin/Debug、obj 目录只保留 .sln、.csproj、.cs、App.config、SQL 脚本。对方拿到手后直接用 Visual Studio 打开 .sln 就能编译而不是被一堆编译中间文件干扰。打包之前最后用记事本打开一遍 App.config看连接字符串里的密码是不是测试密码如果是 sa 弱口令至少提醒对方上线前改掉。宿舍管理系统虽然不涉及核心资产但宿管员的电脑通常连着学校内网能省的事故还是提前省掉。这些细节我踩过太多现在习惯把验证清单做成一个 md 文件放压缩包根目录对方照着打勾就行。希望帮到你。本文还有配套的精品资源点击获取
返回列表