
简介一份面向使用C#进行Windows窗体开发的学习者的实验器材管理系统完整源码包基于经典三层架构UI/BLL/DAL与Models实体层搭配SQL Server数据库实现。系统覆盖管理员登录注册、密码修改、器材信息增删改查及借还登记等核心流程查询模块支持按编号精确匹配、器材名模糊检索、按种类过滤和库存排序适合作为课程设计或毕业设计的参考原型。压缩包共135个文件以C#源码(.cs)、项目配置(.csproj)、编译依赖(.dll)、窗体资源(.resx/.resources)及可执行文件(.exe)为主附带说明文档与数据模型定义整体仅约916KB结构清晰易于导入Visual Studio后直接调试。已有131人学习或下载。借助三层框架的分层示例、完整数据操作代码和配套演示视频可快速理解业务逻辑与数据访问的分离方式并在现有功能上继续扩展库存预警、器材分类维护等模块。1. 把它当练手样板C# Windows 三层架构实验器材管理系统里有什么一个 C# Windows 三层架构的实验器材管理系统最值得看的地方不是界面而是登录注册、器材增删改查、借还登记这些业务从 SQL Server 数据库到界面到底怎么串起来。这套资源把 UIBSM、BLL、DAL、Models 拆成四个独立工程数据和流程都能直接改是典型的 C/S 三层架构样板。适合刚学完 C# 语法、想看看真实项目长什么样的学生也适合需要一套能改的管理系统底子、不想从零搭架构的新手工程师。下文按三层怎么分工、用户模块、器材管理、借还事务、避坑、验证技巧的顺序拆每一步都放可抄的代码和参数说明。2. 三层架构分工与用户模块Models、DAL、BLL 怎么协作三层架构的核心约束一句话就能讲完界面层不写 SQL数据访问层不弹对话框业务逻辑层做校验和编排。这套系统的四个工程把这条约束落实得很清楚我拿到项目第一件事就是看 csproj 列表先把层分出来再顺着调用链把每个方法归位。2.1 从 csproj 文件还原项目结构四个工程各干一件事文件列表里能看到 BSM.csproj、BSM.BLL.csproj、BSM.DAL.csproj、BSM.Models.csproj 四个工程文件依赖关系是单向的我用一张表直接说清工程层职责允许引用BSM.csprojUI 层WinForms 窗体、登录注册界面、列表绑定、弹窗提示BLL、ModelsBSM.BLL.csproj业务逻辑层数据校验、业务流程编排、组合 DAL 结果DAL、ModelsBSM.DAL.csproj数据访问层连接数据库、执行 SQL、返回实体或集合ModelsBSM.Models.csproj模型层实体类只存数据不带逻辑无常见误用是 UI 里直接 new SqlConnection或者 DAL 里弹 MessageBox。这两种写法在课设里出现得很多但一旦界面换掉或者数据库换掉改动会牵一发动全身。照着这份资源的工程结构做每个类只干一类事后面加功能会舒服很多。2.2 Models 层把用户、器材写成实体类Models 层最简单也最容易被跳过。实体类字段和表列名尽量保持一致后续 SqlDataReader 按列名取值时不用做映射。用户和器材两个实体是这样namespace BSM.Models { public class User { public int Id { get; set; } public string UserName { get; set; } public string Password { get; set; } public DateTime CreateTime { get; set; } } public class Equipment { public int Id { get; set; } public string EquipmentNo { get; set; } // 器材编号 public string EquipmentName { get; set; } // 器材名称 public string Category { get; set; } // 器材种类 public int Stock { get; set; } // 库存 public string Location { get; set; } // 存放位置 } }参数说明Id 对应数据库自增主键EquipmentNo 是界面上的业务主键删除、查询、借还都以它为准。Password 用 string 明文放着的确不好但这是练手项目的常态生产环境至少要做哈希加盐这一点后面避坑章再展开。2.3 DAL 层用户注册和登录只有三条 SQLDAL 层每张表对应一个类每个方法对应一种数据操作。用户这块需要注册INSERT、按用户名查SELECT、改密码UPDATE。给出注册和查询的完整写法注意 DAL 项目要引用 System.Configurationnamespace BSM.DAL { public class UserDAL { private readonly string connStr ConfigurationManager.ConnectionStrings[BSMConn].ConnectionString; public int Insert(User user) { string sql INSERT INTO Users(UserName, Password, CreateTime) VALUES(UserName, Password, GETDATE()); using (SqlConnection conn new SqlConnection(connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(UserName, user.UserName); cmd.Parameters.AddWithValue(Password, user.Password); conn.Open(); return cmd.ExecuteNonQuery(); } } public User GetByUserName(string userName) { string sql SELECT Id, UserName, Password, CreateTime FROM Users WHERE UserName UserName; using (SqlConnection conn new SqlConnection(connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(UserName, userName); conn.Open(); using (SqlDataReader reader cmd.ExecuteReader()) { if (reader.Read()) { return new User { Id (int)reader[Id], UserName reader[UserName].ToString(), Password reader[Password].ToString(), CreateTime Convert.ToDateTime(reader[CreateTime]) }; } } } return null; } } }几个参数要点所有值都走 SqlParameter不拼字符串避免单引号注入和中文转义问题ExecuteNonQuery 返回受影响行数注册成功会返回 1ExecuteReader 用 using 包住数据读完连接能及时归还连接池。UpdatePassword 就是一条UPDATE Users SET PasswordNewPwd WHERE UserNameUserName写法同上不再重复贴。2.4 BLL 层把“查一下”变成“能不能登录”BLL 层存在的意义是把 DAL 的单个动作组合成业务规则。同样是查用户DAL 只回答“这个用户存不存在”BLL 才判断“密码对不对、能不能登录”。注册时还要先查重、再插入namespace BSM.BLL { public class UserManager { private readonly UserDAL dal new UserDAL(); public bool Register(User user) { if (string.IsNullOrWhiteSpace(user.UserName) || string.IsNullOrWhiteSpace(user.Password)) { throw new ArgumentException(用户名和密码不能为空); } if (dal.GetByUserName(user.UserName) ! null) { throw new InvalidOperationException(该用户名已存在); } return dal.Insert(user) 0; } public bool Login(string userName, string password) { User user dal.GetByUserName(userName); return user ! null user.Password password; } public bool ChangePassword(string userName, string oldPwd, string newPwd) { User user dal.GetByUserName(userName); if (user null) throw new InvalidOperationException(用户不存在); if (user.Password ! oldPwd) throw new InvalidOperationException(原密码错误); return dal.UpdatePassword(userName, newPwd) 0; } } }这样 UI 层调用 Login 时不需要知道背后是 SELECT 还是存储过程。以后想改成“连续输错五次锁定账号”也只需要在 BLL 里加计数逻辑界面和数据库都不用动。这就是把业务规则放在中间层而不是界面的好处。3. 器材信息管理增删改查与三种查询方式的实现器材信息模块是这套系统的重头戏功能列表里八项五项落在器材上。建表时把器材种类和库存单拎出来是因为后面查询和排序会直接用到这两个字段。3.1 器材表结构编号做主键还是自增主键器材表在 SQL Server 里我会这样建CREATE TABLE Equipment ( Id INT IDENTITY(1,1) PRIMARY KEY, EquipmentNo NVARCHAR(50) NOT NULL UNIQUE, EquipmentName NVARCHAR(100) NOT NULL, Category NVARCHAR(50) NOT NULL, Stock INT NOT NULL DEFAULT 0, Location NVARCHAR(100) NULL );Id 是物理主键EquipmentNo 是业务主键。界面上的添加、删除、查询、借还全部以 EquipmentNo 为准这样即使将来合并数据编号不会和自增 Id 冲突。Stock 默认 0 给“新器材还没入库先建档案”的场景兜底。添加器材就是 INSERT删除就是DELETE FROM Equipment WHERE EquipmentNo No两条语句都是参数化写法和用户模块的 Insert 基本一致区别只在表名和字段INSERT INTO Equipment(EquipmentNo, EquipmentName, Category, Stock, Location) VALUES(No, Name, Category, Stock, Location);参数取值范围和 2.3 节一样字符串全部走 SqlParameter。特别注意 EquipmentNo 是从界面文本框读进来的编号别拿用户输入直接拼 SQL否则器材编号里出现单引号直接炸。3.2 编号精确查询与名称模糊查询LIKE 参数的正确拼法编号精确查询最简单WHERE EquipmentNo No返回单个实体还是 null 由调用方判断。名称模糊查询是这套系统里第一个容易翻车的点public ListEquipment SearchByName(string name) { string sql SELECT * FROM Equipment WHERE EquipmentName LIKE Name; using (SqlConnection conn new SqlConnection(connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { // % 号加在参数值里不是加在 SQL 字符串里 cmd.Parameters.AddWithValue(Name, % name.Trim() %); conn.Open(); ListEquipment list new ListEquipment(); using (SqlDataReader reader cmd.ExecuteReader()) { while (reader.Read()) { list.Add(new Equipment { Id (int)reader[Id], EquipmentNo reader[EquipmentNo].ToString(), EquipmentName reader[EquipmentName].ToString(), Category reader[Category].ToString(), Stock (int)reader[Stock], Location reader[Location] DBNull.Value ? : reader[Location].ToString() }); } } return list; } }重点说明写成WHERE EquipmentName LIKE %Name%是错的SQL Server 会把 Name 当成字面量的一部分结果一条都查不到。正确做法是把 % 拼进参数值SQL 里只保留 Name 占位符。另外记得对输入做 Trim()很多人复制粘贴的时候会带上空格导致明明有数据却查不到。3.3 按种类查询与库存排序ORDER BY 只能走白名单按种类查询是精确匹配WHERE Category Category。库存排序显示这个需求排序字段不能参数化只能拼 SQL所以要做白名单判断public ListEquipment SearchByCategory(string category, string stockOrder) { string sql SELECT * FROM Equipment WHERE Category Category; if (stockOrder asc) sql ORDER BY Stock ASC; else if (stockOrder desc) sql ORDER BY Stock DESC; // 其余情况不排序 using (SqlConnection conn new SqlConnection(connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(Category, category); // 执行并读取逻辑同 SearchByName这里省略 } }ORDER BY 直接拼接确实有注入风险所以只允许传入 asc 和 desc 两个固定值界面下拉框也只给这两个选项。如果你想把排序字段也做成参数SQL Server 侧要用存储过程和 CASE WHEN 映射练手项目没必要上白名单够用。3.4 修改器材信息别再写一条 SQL 把没填的字段覆盖掉修改器材信息最常见的错误写法把界面上所有文本框都丢进 UPDATE没填的字段变成了空字符串。我一般用“先查后改”先把原记录查出来回填到界面用户改了哪项提交时就覆盖哪项。public bool Update(Equipment eq) { string sql UPDATE Equipment SET EquipmentNameName, CategoryCategory, StockStock, LocationLocation WHERE EquipmentNoNo; using (SqlConnection conn new SqlConnection(connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(Name, eq.EquipmentName); cmd.Parameters.AddWithValue(Category, eq.Category); cmd.Parameters.AddWithValue(Stock, eq.Stock); cmd.Parameters.AddWithValue(Location, eq.Location); cmd.Parameters.AddWithValue(No, eq.EquipmentNo); conn.Open(); return cmd.ExecuteNonQuery() 0; } }界面修改逻辑对应先按编号查出来再把查询对象里的属性逐个赋值为界面文本框的值最后调 Update。这样没有填的字段会保留原值而不是被 NULL 覆盖。这条经验在维护别人的系统时特别有用改器材信息比删除器材信息更容易踩坑因为问题隐蔽用户往往要过几天才发现数据不对。4. 借还与库存更新为什么要用 SqlTransaction借还是这套系统里唯一一个“一次操作要同时写两张表”的功能。借出要扣器材库存、写借还登记归还要更新登记状态、回补器材库存。多个 SQL 在一个操作里完成就必须考虑事务。4.1 借出流程把库存判断写进 UPDATE 的 WHERE借出的业务目标只有两件事把器材库存扣掉、登记一条借出记录。库存够不够不必单独 SELECT 去查把判断条件直接写进 UPDATE 的 WHERE 就行string updateSql UPDATE Equipment SET Stock Stock - Qty WHERE EquipmentNo No AND Stock Qty;这样数据库在扣减的同时判断库存够不够受影响行数为 0 就说明库存不足直接回滚。常见翻车写法是先 SELECT 查库存C# 里 if 判断够不够再 UPDATE 扣减。这个写法单用户没事多用户同时借同一台示波器时两次 SELECT 都读到库存为 2然后都执行扣减库存就变成 0 而不是应该的 1。把并发问题交给数据库解决比在 C# 代码里加锁简单可靠。4.2 归还流程先更新登记再回补库存归还时先按记录 Id 把那条未归还的记录改成已归还再回补器材库存。顺序不能反如果先回补库存登记更新失败账面上库存多了、借还记录还是未还数据就对不上了。归还登记表结构可以这样设计字段类型说明IdINT IDENTITY主键EquipmentNoNVARCHAR(50)对应器材编号BorrowerNVARCHAR(50)借用人QtyINT借出数量BorrowDateDATETIME借出时间ReturnDateDATETIME NULL归还时间NULL 表示未还StatusINT0 未归还1 已归还归还的 SQL 顺序先UPDATE BorrowRecord SET Status1, ReturnDateGETDATE() WHERE IdRecordId AND Status0再UPDATE Equipment SET Stock Stock Qty WHERE EquipmentNo No。4.3 事务写法把三条 SQL 包进同一个 SqlTransaction把借出流程写成完整代码注意 SqlCommand 复用时的参数处理public void Borrow(string equipmentNo, string borrower, int qty) { using (SqlConnection conn new SqlConnection(connStr)) { conn.Open(); SqlTransaction tran conn.BeginTransaction(); using (SqlCommand cmd new SqlCommand()) { cmd.Connection conn; cmd.Transaction tran; try { cmd.CommandText UPDATE Equipment SET Stock Stock - Qty WHERE EquipmentNo No AND Stock Qty; cmd.Parameters.AddWithValue(Qty, qty); cmd.Parameters.AddWithValue(No, equipmentNo); int affected cmd.ExecuteNonQuery(); if (affected 0) throw new InvalidOperationException(库存不足借出失败); cmd.CommandText INSERT INTO BorrowRecord(EquipmentNo, Borrower, Qty, BorrowDate, Status) VALUES(No, Borrower, Qty, GETDATE(), 0); cmd.Parameters.Clear(); // 关键换 SQL 前清掉旧参数 cmd.Parameters.AddWithValue(No, equipmentNo); cmd.Parameters.AddWithValue(Borrower, borrower); cmd.Parameters.AddWithValue(Qty, qty); cmd.ExecuteNonQuery(); tran.Commit(); } catch { tran.Rollback(); throw; } } } }两个容易忽略的细节一是复用 SqlCommand 改 CommandText 时必须 Parameters.Clear()否则会残留 Qty、No 这些上一轮参数甚至报“变量名 Qty 已被声明”的错误二是事务里任何一步抛异常Rollback 会把扣库存的操作一起撤销保证借还记录和库存永远一致。这套手法的适用范围不只是借还凡是“一次点击改两张表”的场景比如器材入库同时更新供应商记录都应该照这样包事务。5. 避坑与常见问题跑这套系统必须绕开的五个坑这个项目是个练手样板但数据库连接、项目引用、SQL 写法这些环节每个都是新手容易卡住的地方。下面按出现频率排序记录五条都是我实际遇到过、也看别人反复踩的。5.1 数据库附加不上多半是版本对不上现象双击项目里的 .mdf 附加到 Visual Studio 时报错提示“数据库 BSM 的版本高于此实例所支持的版本”或者“文件正由另一进程使用”。原因本机 Visual Studio 内置 LocalDB 的版本低于建库时的 SQL Server 版本或者上一次调试进程没有释放数据库文件。解决先用 SQL Server Management Studio 附加数据库确认版本附加时若报“文件正被占用”打开任务管理器结束残留的 sqlservr.exe 子进程。连接字符串优先用Data Source.\SQLEXPRESS;Initial CatalogBSM;Integrated SecurityTrue如果本机只装了 LocalDB 就改用(localdb)\MSSQLLocalDB。资源里带了演示视频对照视频里的实例名直接抄最稳。5.2 中文模糊查询查不到记录现象器材名称里明明有“示波器”输入“示波”查出来是空。原因最常见两种情况一是表字段建成了 VARCHAR 而不是 NVARCHAR中文被转成问号存进去二是输入法输入时带了不可见字符Trim() 没做干净。解决建表时字符串字段用 NVARCHAR这是治本的。已经建好的表执行一条 ALTER TABLE 把字段类型改掉已经乱码的数据只能清掉重录没救。查询代码里对关键字先Trim()如果还查不到把最终执行的 SQL 和参数值打印出来看一眼直接定位是编码问题还是空格问题。5.3 编译报错UI 里找不到 BLL 的类型现象窗体代码里写下UserManager userManager new UserManager();编译器报“未能找到类型或命名空间 UserManager”。原因BSM 主工程没有添加对 BLL 工程的引用或者 BLL 没有引用 DAL导致类型不可见。解决右键 BSM 工程 → 添加 → 项目引用勾选 BSM.BLL 和 BSM.Models。记住引用方向是单向的BSM → BLL → DAL → Models。如果出现 DAL 引用 UI、BLL 引用 BSM 这种环形引用Visual Studio 会直接拒绝把多余的引用删掉重加就行。5.4 借出后库存变成负数现象库存显示 1连续借出三次界面变成 -2。原因借出逻辑只写了 SELECT 查库存C# 判断够了就 UPDATE 扣减。两个窗口同时操作时后一个判断读到的是旧值扣减就超了。解决把判断塞进 UPDATE 的 WHERE 条件写成UPDATE Equipment SET Stock Stock - Qty WHERE EquipmentNo No AND Stock Qty受影响行数为 0 就提示库存不足并回滚。这个写法由数据库自己保证原子性比任何 C# 里的 if 判断都可靠。5.5 DataGridView 操作后不刷新现象新增、修改、删除器材后界面列表还是旧数据重启程序才能看到最新结果。原因DataGridView 绑定的 DataTable 还是第一次查询时的实例数据源对象没变界面自然不会重绘。解决每次业务操作成功后重新执行查询并重新给 DataSource 赋值赋值前先置 null。这是解决列头和内容不同步的常用手段dataGridView1.DataSource null; dataGridView1.DataSource manager.SearchByName(txtKey.Text.Trim());加上那句 null 之后列头不会出现新增列挤在一起的错位问题。如果列表还带序号列也要在重新绑定后重绘一下行号。6. 一个值得保留的验证技巧把 BLL 层当黑匣子逐个打点界面难调试是因为问题会发生在“按钮事件 → BLL → DAL → 数据库”的任何一环还经常伴随界面线程的干扰。我的做法是先不开窗体直接建一个临时控制台项目引用 BLL 和 Models把核心方法按业务顺序打一遍static void Main(string[] args) { var userManager new UserManager(); var reg userManager.Register(new User { UserName tester, Password 123456 }); Console.WriteLine(注册: reg); Console.WriteLine(登录: userManager.Login(tester, 123456)); var eqManager new EquipmentManager(); var eq new Equipment { EquipmentNo EQ001, EquipmentName 示波器, Category 仪器, Stock 5, Location A01柜 }; Console.WriteLine(新增器材: eqManager.AddEquipment(eq)); Console.WriteLine(借出: eqManager.Borrow(EQ001, 张三, 2)); // 先查到未归还记录的 Id再调用归还这里用 1 占位 Console.WriteLine(归还: eqManager.ReturnEquipment(1, EQ001, 2)); }为什么这么干三层架构中 BLL 的公共方法是稳定契约控制台项目里每个方法返回是否符合预期一眼就能看到。如果控制台里注册、登录、借出、归还全部正常那问题就锁定在 WinForms 事件或绑定逻辑上如果控制台里就抛异常多半是数据库连接、SQL、事务这三件事之一按避坑章的顺序排查。我还习惯补几组边界测试库存为 0 时借出、注册重复用户名、改密码时原密码错误。这些场景在界面上逐个点要开好几个窗口控制台里只需要两三行代码能直接确认异常是否抛得准确。从那以后我每次拿到别人的 C# 三层项目都会先建这个临时控制台把 BLL 的关键接口打一遍确认核心逻辑通顺再开界面省掉大约一半的断点调试时间。这份实验器材管理系统也建议你按这个顺序跑一遍先控制台、后界面、再看演示视频对照希望帮到你。本文还有配套的精品资源点击获取