C#三层架构实验器材管理系统:数据库设计与事务处理实战

发布时间:2026/10/9 6:31:35
C#三层架构实验器材管理系统:数据库设计与事务处理实战 简介C# Windows三层架构实验器材管理系统完整项目配套SQL Server数据库面向需要完成课程设计或毕业设计的计算机专业学生。系统涵盖用户登录注册、密码修改、器材信息增删改查、库存排序显示、借还登记与库存更新等完整业务闭环适合从中理解三层的UI、BLL、DAL、Models分层协作方式以及SQL Server数据库交互和常见增删查改写法。压缩包共135个文件含44个cs源码、14个dll、13个exe及config配置等包体仅916KB便于快速下载与本地部署同时包含sln工程文件、cache等生成物结构和完整度较高可用于直接打开运行或对照学习。目前已有131人浏览学习适合入门至中级C# Windows开发者作为参考。压缩包内提供演示视频网盘链接结合代码可快速还原系统效果也可作为实验报告或功能演示的素材基础。1. 实验器材管理系统C# Windows 三层架构带数据库到底解决什么问题接到这类需求时我通常先问一句这套实验器材管理系统是给谁用的数据库准备落在哪台机器上。C# Windows 三层架构带数据库是课程设计、毕业设计和小型实验室内部工具最常走的路子实验室管理员要查器材在哪、借给谁了、库存还剩多少老师要看借用记录而开发者希望这套东西后续能加权限、接扫码枪、换数据库都不至于推倒重来。反直觉的结论是这个系统功能并不难难的是三层架构经常被写成伪三层——SQL 直接写在窗体按钮事件里业务规则散落在各个页面中。这篇笔记我会把一个可复现的最小方案从头拆到尾项目结构怎么搭、数据库怎么设计、登录和器材的数据库增删改查怎么写、借还操作的事务怎么处理以及新手最容易踩的连接串、并发扣库存、ID 取回这几个坑。适合正在做课程设计的学生也适合给实验室做内部工具但不想把代码写死的一线工程师。2. 三层架构拆解项目结构怎么搭才算不是伪三层2.1 伪三层是怎么写出来的我见过不少学员交上来的实验器材管理系统项目里确实分了好几个文件夹但点开一看窗体里直接new SqlConnectionSQL 语句用字符串拼出来业务判断写在一百多行的按钮点击事件里。这种代码不是三层架构只是把一堆代码按文件位置分了分类。伪三层最大的问题不是难看是改不动想加一个“非管理员不能删除器材”的规则得在一个个窗体里反复找 SQL想从 SQL Server 换成另一个数据库所有页面要重写连接逻辑。真正的三层架构核心是让每一层只干一件事并且让依赖方向单向流动。表现层UI负责收集输入、显示结果业务逻辑层BLL负责校验规则、流程控制数据访问层DAL只负责和数据库打交道返回实体对象或数据集。实体模型Model是三层共享的“信使”不包含任何逻辑。三层的引用关系是固定的UI 引用 BLLBLL 引用 DAL三层都引用 ModelUI 不允许直接引用 DAL。2.2 一个可以直接照抄的解决方案结构我一般在 Visual Studio 里建一个解决方案下面放四个项目每个项目的作用和引用方向如下表项目职责引用谁Model用户、器材、借用记录的实体类无DAL所有 SQL 语句、连接管理、参数化增删改查ModelBLL登录校验、库存判断、权限判断、流程控制DAL, ModelUIWinForms 窗体、用户控件、数据绑定BLL, Model解决方案的目录结构大致是这样ExperimentManager.sln |-- ExperimentManager.Model | |-- SysUser.cs | |-- DeviceInfo.cs | |-- BorrowRecord.cs |-- ExperimentManager.DAL | |-- SqlHelper.cs | |-- SysUserDAL.cs | |-- DeviceInfoDAL.cs | |-- BorrowRecordDAL.cs |-- ExperimentManager.BLL | |-- SysUserBLL.cs | |-- DeviceInfoBLL.cs | |-- BorrowRecordBLL.cs |-- ExperimentManager.UI |-- LoginForm.cs |-- MainForm.cs |-- DeviceManageForm.cs |-- BorrowRecordForm.cs |-- App.config这里的逻辑说明DAL 里的SqlHelper负责连接字符串读取和公共的数据库操作封装每个具体的 DAL 类只写某一类业务数据的增删改查BLL 层调用 DAL 时先做业务判断再决定要不要执行数据操作UI 层只负责调用 BLL 并展示结果永远不直接接触SqlConnection。参数说明连接字符串只出现在App.config里DAL 统一从配置读取这样部署到实验室的电脑时只需要改配置文件不需要重新编译项目。2.3 落地习惯先定实体和接口再写页面我一般会把开发顺序定成先建数据库再写 Model 实体类然后写 DAL再写 BLL最后才做窗体。只要实体类和 DAL 方法的命名稳定下来UI 层怎么写都不会乱。命名上有一个比较省事的约定DAL 方法统一叫Add、Update、Delete、QueryById、QueryList不要出现一个叫DeleteDevice、另一个叫RemoveDeviceInfo的情况。窗体控件的命名也建议用“类型简称语义名”比如txtDeviceName、lblStock、dgvDeviceList这就是 C# 控件命名里常见的类型前缀约定别图省事叫textBox1、textBox2否则界面一复杂自己都分不清哪个是名称、哪个是库存。这套结构还有一个好处BLL 层是独立于数据库的。如果实验室要求换数据库只需要改写 DAL 层方法实现BLL 和 UI 完全不动。这就是三层架构在实验器材管理系统里最实在的价值不是架构好看是后续改得动、查得清。3. 数据库设计与连接先定表结构再谈连接字符串3.1 三张表足矣用户、器材表、借用记录表实验器材管理系统的核心数据模型三张表就够了。第一张SysUser用户表字段为主键、用户名、密码哈希、角色、显示姓名第二张DeviceInfo器材表存器材编号、名称、分类、型号、库存数量、存放位置、状态第三张BorrowRecord借用记录表记录哪个用户借了哪个器材、借了多少、借的时间和归还的时间。这三张表之间的关系是器材表和借用记录表是 1 对 N用户表和借用记录表也是 1 对 N借用记录表像一张“中间台账”把用户和器材通过记录连起来。下面的建表脚本可以直接在 SQL Server Management Studio 里执行CREATE TABLE SysUser ( Id INT IDENTITY(1,1) PRIMARY KEY, UserName NVARCHAR(50) NOT NULL UNIQUE, PasswordHash VARBINARY(64) NOT NULL, Role NVARCHAR(20) NOT NULL DEFAULT Student, DisplayName NVARCHAR(50) NOT NULL ); CREATE TABLE DeviceInfo ( Id INT IDENTITY(1,1) PRIMARY KEY, DeviceNo NVARCHAR(30) NOT NULL UNIQUE, DeviceName NVARCHAR(100) NOT NULL, Category NVARCHAR(50), Model NVARCHAR(50), Stock INT NOT NULL DEFAULT 0, Location NVARCHAR(100), Status NVARCHAR(20) NOT NULL DEFAULT Available ); CREATE TABLE BorrowRecord ( Id INT IDENTITY(1,1) PRIMARY KEY, UserId INT NOT NULL REFERENCES SysUser(Id), DeviceId INT NOT NULL REFERENCES DeviceInfo(Id), Quantity INT NOT NULL, BorrowTime DATETIME NOT NULL, ReturnTime DATETIME NULL, Status NVARCHAR(20) NOT NULL DEFAULT Borrowed );这段脚本的逻辑说明Id全部用自增主键避免业务编号做主键出现数据重复和修改麻烦PasswordHash用VARBINARY(64)存储哈希值不要用明文密码BorrowRecord的ReturnTime允许为空为空就表示还没归还。参数说明Quantity表示借的数量归还时更新ReturnTime并加回库存器材表的Status字段可以存Available、Borrowed、Scrapped等状态不要用数字存状态否则看代码的人不知道 1 是代表可用还是报废。3.2 连接字符串怎么填从本机到实验室服务器连接字符串写在 UI 项目的App.config里开发时最省心的写法是下面这样connectionStrings add nameDbConnectionString connectionStringData Source.;Initial CatalogExperimentDB;User IDsa;Passwordyour_password;EncryptFalse;TrustServerCertificateTrue; providerNameSystem.Data.SqlClient / /connectionStrings这个配置的说明Data Source.表示本机默认实例如果你在开发机上装的是 SQL Server Express要改成.\SQLEXPRESS如果数据库在实验室的服务器上要写成服务器的 IP 地址比如Data Source192.168.1.20。Initial Catalog是数据库名称。EncryptFalse和TrustServerCertificateTrue这两个参数在本地开发时能避免很多加密连接导致的报错生产环境再重新评估要不要开加密连接。DAL 层读取这个配置的代码通常是public static string GetConnectionString() { return ConfigurationManager.ConnectionStrings[DbConnectionString].ConnectionString; }注意这里需要引用System.Configuration.dll。很多新手在这一步会报“未命名配置节”或者NullReferenceException多半是因为App.config里没有写connectionStrings节点或者节点名写错。连接字符串千万别硬编码在代码里否则每台电脑部署都要重新编译。3.3 用 ADO.NET 还是 EF小系统我为什么偏向前者给课程设计或内部工具做实验器材管理系统我一般直接用 ADO.NET 加参数化 SQL而不是引入 EF Core。原因有三第一ADO.NET 没有任何额外包依赖创建项目就能写不用处理数据库迁移和导航属性的概念第二这个系统核心就是增删改查用 SQL 写更直观报错时也更容易定位第三很多课程设计的答辩老师会重点提问 SQL 语法和事务处理你能讲清楚自己的 SQL 和事务比笼统说“EF 帮我做的”要有说服力。如果你的环境是 MySQLDAL 层方法结构完全可以保留只需把System.Data.SqlClient换成MySql.Data.MySqlClient以及对应连接串格式BLL 和 UI 不用动。这本身就是分层设计带来的好处DAL 层是唯一和具体数据库有关的模块。4. 登录与器材的增删改查从 Model 到窗体的完整调用链4.1 登录功能密码哈希与角色判断登录是整个系统第一个要落地的功能。设计原则是UI 层只把用户名密码交给 BLLBLL 负责判断用户是否存在、密码对不对、角色是什么。先写实体类SysUser它对应SysUser表public class SysUser { public int Id { get; set; } public string UserName { get; set; } public byte[] PasswordHash { get; set; } public string Role { get; set; } public string DisplayName { get; set; } }实体类的说明属性名称和数据库字段一一对应字段类型要严格匹配数据库的INT对应intVARBINARY(64)对应byte[]。密码不要在这里存明文文本。然后写 DAL 层的查询方法public SysUser GetUserByUserName(string userName) { string sql SELECT Id, UserName, PasswordHash, Role, DisplayName FROM SysUser WHERE UserName UserName; using (SqlConnection conn new SqlConnection(SqlHelper.GetConnectionString())) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(UserName, userName); conn.Open(); using (SqlDataReader reader cmd.ExecuteReader()) { if (reader.Read()) { return new SysUser { Id reader.GetInt32(0), UserName reader.GetString(1), PasswordHash (byte[])reader[2], Role reader.GetString(3), DisplayName reader.GetString(4) }; } return null; } } }这段代码的逻辑说明用参数化查询把UserName作为参数传入避免了字符串拼接引发的 SQL 注入问题整段数据库访问都包含在using块里连接和命令用完会立即释放这是防止连接池耗尽的最基本习惯。参数说明using嵌套的顺序是先连接后命令释放时会从内向外关闭SqlDataReader也放在using里否则它会一直占着连接通道。BLL 层的登录校验逻辑是先在内存里比对哈希值而不是把密码明文传到 SQL 里再比。常见的做法是用 SHA256 生成哈希将用户输入的密码做同样的哈希处理后再和数据库里的哈希逐字节比对。这样即使数据库文件被拷走也没有人能直接看到密码。4.2 器材的数据库增删改查DAL 层四件套写清楚器材管理的核心是数据库增删改查DAL 层提供一个DeviceInfoDAL类里面写四个方法Add、Update、Delete、QueryList。先看新增器材的代码public int Add(DeviceInfo device) { string sql INSERT INTO DeviceInfo (DeviceNo, DeviceName, Category, Model, Stock, Location, Status) VALUES (DeviceNo, DeviceName, Category, Model, Stock, Location, Status); SELECT SCOPE_IDENTITY();; using (SqlConnection conn new SqlConnection(SqlHelper.GetConnectionString())) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(DeviceNo, device.DeviceNo); cmd.Parameters.AddWithValue(DeviceName, device.DeviceName); cmd.Parameters.AddWithValue(Category, (object)device.Category ?? DBNull.Value); cmd.Parameters.AddWithValue(Model, (object)device.Model ?? DBNull.Value); cmd.Parameters.AddWithValue(Stock, device.Stock); cmd.Parameters.AddWithValue(Location, (object)device.Location ?? DBNull.Value); cmd.Parameters.AddWithValue(Status, device.Status); conn.Open(); return Convert.ToInt32(cmd.ExecuteScalar()); } }这段代码的逻辑说明插入器材后立刻用SCOPE_IDENTITY()拿回新记录的自增主键这个返回值直接作为方法结果返回UI 层拿到后可以用于后续操作比如新增完自动加载该器材的借用页面。参数说明可空字段在传给SqlParameter时必须处理为DBNull.Value否则会报“不能将 null 赋值给 SqlParameter.Value”。这里用了(object)device.Category ?? DBNull.Value的模式这是 C# 里处理可空数据库字段最省心的写法。BLL 层的调用方法也很简单先做规则校验再交给 DALpublic bool AddDevice(DeviceInfo device) { if (string.IsNullOrWhiteSpace(device.DeviceNo)) throw new ArgumentException(器材编号不能为空); if (device.Stock 0) throw new ArgumentException(库存数量不能为负数); int newId _deviceDAL.Add(device); return newId 0; }逻辑说明BLL 层不写 SQL只做业务判断和流程控制。如果后续要限制只有管理员能添加器材只需在 BLL 层的调用处加角色判断UI 层和 DAL 层都不用改。4.3 UI 层只做输入输出窗体、验证与数据绑定最后一步才是写窗体。登录窗体LoginForm里按钮点击事件的做法是private void btnLogin_Click(object sender, EventArgs e) { string userName txtUserName.Text.Trim(); string password txtPassword.Text; SysUserBLL userBLL new SysUserBLL(); SysUser user userBLL.Login(userName, password); if (user null) { MessageBox.Show(用户名或密码错误); return; } CurrentUserInfo.User user; DeviceManageForm mainForm new DeviceManageForm(); mainForm.Show(); this.Hide(); }这一段的事件代码逻辑说明UI 层只做三件事——收集输入、调用 BLL、展示结果。CurrentUserInfo是一个静态类用来存放当前登录用户其他窗体随时可以读取当前用户的角色做界面级控制比如管理员才显示删除按钮。器材列表的展示同样简单BLL 返回DataTable或ListDeviceInfoUI 层把它直接绑定到DataGridView的数据源。所有 UI 层的代码里不允许出现连接字符串、SQL 语句和SqlConnection一旦出现这条纪律就说明三层架构又被写回伪三层了。5. 避开实验器材管理系统开发中的五个常见坑5.1 连接串与数据库访问类的坑第一个坑是连接字符串明明照着教程写的运行时却报“建立与 SQL Server 的连接时发生网络相关错误”。现象是开发机能连装到实验室的电脑就连不上。原因通常是Data Source写成了localhost或本机名而目标电脑上的 SQL Server 实例名带版本后缀或者服务器没开 TCP/IP 协议。解决方法是先确认目标电脑 SQL Server 的实例名在命令行输入sqlcmd -L查看本机实例列表然后把连接串里的Data Source改成实例名或 IP例如Data Source192.168.1.20\SQLEXPRESS。另一个高频坑是密码里有特殊字符没转义连接串报“关键字不受支持”这种直接检查Password值是否用了分号或引号替换掉即可。第二个坑是程序跑一段时间后数据库操作突然卡死报“超时时间已到”。原因多数是SqlConnection没有释放连接池被占满。这个问题的判断方法很简单打开 SQL Server 的“活动监视器”能看到大量处于休眠状态的连接。解决办法是全面检查代码把所有数据库访问都改成using块包裹严禁手动conn.Open()后忘记关闭。这里有一个血泪经验SqlDataReader没关会占着连接不放即使连接本身写进了using只要reader没用using包裹连接依然无法正常归还池子。第三个坑是执行带空值的插入更新时报“不能将 null 赋值给 SqlParameter.Value”。现象是字段输入留空就报错。原因是 C# 的可空字符串类型在传参时没有转成DBNull.Value。解决办法就是前面代码里演示的(object)device.Category ?? DBNull.Value把这种转换写成一个扩展方法所有 DAL 方法统一调用能省掉大量重复判断。5.2 业务逻辑层的几个坑第四个坑是并发借器材时库存被扣成负数。现象是一台仪器只剩一台两个学生同时提交借用申请系统都显示成功。原因是最简单的UPDATE DeviceInfo SET Stock Stock - Quantity没有做库存下限判断两个事务同时读到旧库存互相覆盖。解决方案是把扣库存的 SQL 改成条件更新UPDATE DeviceInfo SET Stock Stock - Quantity WHERE Id Id AND Stock Quantity;逻辑说明Stock Quantity这个条件让数据库在原子层面拦截超卖如果受影响行数为 0说明库存不足BLL 层直接把这次借用判定为失败。这是实验器材管理系统里最需要重视的并发问题比加锁更好用因为它在数据库层面解决不依赖应用程序的锁机制。第五个坑是新插入的记录拿不到自增主键。现象是插入一条器材记录后想借出这件器材但不知道新记录的 Id 是多少。原因是插入语句没有配套的SELECT SCOPE_IDENTITY()或者用了SELECT IDENTITY拿了其他连接生成的 Id。解决办法就是前面Add方法里那样插入语句后面跟上SELECT SCOPE_IDENTITY()用ExecuteScalar()返回自增值。另外还要提醒一下删除操作物理删除器材后历史借用记录会变成“悬空记录”借用记录里的器材信息就查询不到了。更稳妥的做法是加一个IsDeleted位段或把Status改成Scrapped查询列表时默认过滤掉已删除项。这样既保留历史数据又把这套系统做成有“后悔药”可吃而不是一句DELETE删得干干净净。6. 终极彩蛋用事务把借出和归还做成一条完整命令器材借用和归还是这个系统里最值得做成事务的操作。借出一台器材本质是两件事同时发生扣减库存、插入借用记录。归还则是反向的两件事加回库存、更新借用记录的归还时间。如果这两步不包在同一个事务里就会出现“库存扣了但记录没写”或“记录写了但库存没减”的中间状态。我在 BLL 层封装一个BorrowDevice方法接受事务对象作为参数。public bool BorrowDevice(int userId, int deviceId, int quantity) { using (SqlConnection conn new SqlConnection(SqlHelper.GetConnectionString())) { conn.Open(); using (SqlTransaction tx conn.BeginTransaction()) { try { string updateSql UPDATE DeviceInfo SET Stock Stock - Quantity WHERE Id Id AND Stock Quantity; using (SqlCommand cmd new SqlCommand(updateSql, conn, tx)) { cmd.Parameters.AddWithValue(Quantity, quantity); cmd.Parameters.AddWithValue(Id, deviceId); int rows cmd.ExecuteNonQuery(); if (rows 0) { tx.Rollback(); return false; } } string insertSql INSERT INTO BorrowRecord (UserId, DeviceId, Quantity, BorrowTime, Status) VALUES (UserId, DeviceId, Quantity, GETDATE(), Borrowed); using (SqlCommand cmd new SqlCommand(insertSql, conn, tx)) { cmd.Parameters.AddWithValue(UserId, userId); cmd.Parameters.AddWithValue(DeviceId, deviceId); cmd.Parameters.AddWithValue(Quantity, quantity); cmd.ExecuteNonQuery(); } tx.Commit(); return true; } catch { tx.Rollback(); throw; } } } }这段代码的逻辑说明事务从BeginTransaction()开始两个数据库操作共用同一个连接和同一个事务对象任何一个步骤失败都会回滚保证库存和借用记录一致。参数说明cmd构造时第三参数传tx这步非常关键漏掉事务参数的命令默认使用独立的隐式事务等于没包进来。归还操作只需要把更新库存改成加库存再更新ReturnTime和Status结构完全对称。验证这个事务是否生效的方法也很简单在借出代码中途故意抛一个异常看库存是否仍然被扣减如果库存没有变化说明事务生效。如果要给这个系统继续加能力我建议优先做这三件事第一给器材表加一个LastCheckedDate字段用于定期盘点第二把借用记录查询做成带日期筛选的分页查询避免数据量大后界面卡顿第三把登录逻辑里的哈希方案从 SHA256 换成带盐的迭代哈希虽然课程设计里 SHA256 够用但要接真实环境密码存储的强度值得多花几个小时。这个系统的核心价值不在于界面有多漂亮而在于数据是一致、可追溯的。我有一次因为库存判断写在了 UI 层两个窗口同时操作同一台仪器账目直接对不上后来把所有业务规则都收敛到 BLL 层再也没有出过这种问题。希望这些经验能帮你的实验器材管理系统少走几个弯路。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询