简介:这是一套面向C# Windows开发学习者的实验器材管理系统源码,采用经典三层架构(表现层、业务逻辑层、数据访问层)并配套SQL Server数据库,可用于课程设计、毕业设计或理解企业级分层开发。资源共135个文件,包体约916KB,主要包含44个cs源文件、13个dll运行库、3个exe可执行程序及resources/config等配置文件,完整覆盖项目编译、运行与配置所需。系统功能涵盖管理员注册登录、密码修改、器材信息添删改查、按编号/名称/种类多维度查询及借还登记库存更新等,适合练习增删改查与基本业务逻辑。已有131人学习,对想快速搭建WinForms管理项目或梳理三层架构调用关系的开发者较有参考价值。
1. 实验器材管理系统这种C#三层架构项目:为什么我建议你直接抄
做C# WinForms开发的人,迟早会碰到一个带数据库的管理系统需求——今天我拆的这个实验器材管理系统(BSM)就是典型代表。它是基于三层架构搭的,技术栈为C# windows窗体程序,配合SQL Server数据库,核心功能覆盖登录注册、器材信息的增删查改、借还登记这几个高频场景。对刚入门.NET的学生或者需要快速交付管理类小系统的初级工程师来说,这套项目最大的价值不在于功能多,而在于三层架构的分层写法完整、数据库脚本齐全,你可以直接拿它当模板去改造成设备管理系统、图书借阅系统、仓库物料管理系统。
我自己拆完的直观感受是:这类项目的难点从来不是“把窗体画出来”,而是各个层之间怎么解耦、数据库访问怎么封装、借还库存这种状态下怎么保证数据一致。所以这篇文章不只展示它能做什么,还会逐步拆到配置文件、SQL脚本、关键代码实现和几个容易翻车的坑位。如果你正打算自己动手写一个类似的WinForms管理项目,或者做毕业设计需要一套结构清晰的三层系统做基础,那这份资源值得照着复现一遍。
2. 三层架构与BSM项目结构:先明白各层干什么,再谈改代码
2.1 三层架构到底分的是什么
很多教程把三层架构说得玄乎,其实落到这个系统里就四个项目:BSM.Models、BSM.DAL、BSM.BLL、BSM(界面层)。Models放实体类,DAL负责数据库操作,BLL处理业务逻辑,UI层只做界面交互。核心规则是上层引用下层,UI不能直接写SqlConnection,DAL不处理按钮点击,BLL夹在中间做校验和调度。
我一般建议先从BSM.Models开始看,里面的实体类对应数据库表结构,比如器材信息实体会有EquipmentID、EquipmentName、EquipmentType、StockQuantity这类属性。理解实体的意义在于:你后续不管是做模糊查询还是库存排序,数据在层与层之间流转时,都是以这些对象为单位传递,而不是到处传DataTable或者string数组。
2.2 BSM的数据库设计:基础表决定增减改查能不能跑通
这个系统配合SQL Server使用,核心表至少包含管理员账号表、器材信息表、借还记录表。从项目名称“带数据库”以及功能描述看,数据库脚本是随源码一起提供的,这是比某些只给代码不给表的资源强很多的地方。拿到数据库文件后,先执行建库脚本,再往管理员表插入一个初始账号,就能直接跑登录。
三个关键数据表的设计思路可以参考:
| 表名 | 关键字段 | 用途 |
|---|---|---|
| AdminUser | UserId, UserName, Password | 登录注册、密码修改 |
| Equipment | EquipmentId, Name, Type, Stock, Price | 器材信息增删查改 |
| BorrowRecord | RecordId, EquipmentId, UserName, BorrowDate, ReturnDate | 借出归还登记 |
注意器材表里Stock字段是借还逻辑的核心,借出时减少,归还时增加。如果数据库表没有这个字段,后面的库存更新逻辑凭空就做不起来。所以拿到项目后第一步不是去跑界面,而是把数据库表和实体类字段对照一遍,确认一一对应。
2.3 为什么项目文件里有那么多cache文件
细心的人会看到源码目录里有一堆BSM.BLL.csprojAssemblyReference.cache、DesignTimeResolveAssemblyReferencesInput.cache这类文件,这属于Visual Studio编译过程中的中间缓存文件,不影响程序运行,也不是病毒。网上下载的源码常会附带这些,直接忽略即可;如果想要一个干净的工程,可以在资源管理器里搜索*.cache并删除,然后重新打开解决方案。
删除缓存文件的命令可以这样执行:
cd /d 项目根目录 del /s /q *.cache这段命令会递归删除所有.cache文件。删除后双击BSM.sln重新编译,Visual Studio会自动重新生成。需要注意,删除缓存不影响项目还原和编译,但如果你开着VS,先关掉再删,否则文件被占用会提示删除失败。
2.4 连接字符串放在哪:app.config的配置方式
WinForms项目的数据库连接串通常放在app.config里,BSM.DAL层读取它来创建连接对象。拿到项目后第一件事就是修改连接字符串,指向你自己的SQL Server实例。
<connectionStrings> <add name="BSMDB" connectionString="Data Source=.;Initial Catalog=BSMDB;User ID=sa;Password=yourpassword;" providerName="System.Data.SqlClient" /> </connectionStrings>代码里通过ConfigurationManager.ConnectionStrings["BSMDB"].ConnectionString获取这条连接串。注意Data Source=.表示本机默认实例,如果SQL Server用的是命名实例,要写成localhost\\实例名;如果用的是Windows身份验证,则把User ID和Password去掉,改成Integrated Security=True。改完连接串再编译运行,如果还报数据库连接错误,直接用SSMS试一下这个连接串能不能连通,能通就说明问题出在代码层。
3. 登录注册与密码修改:账号模块的完整实现思路
3.1 注册功能为什么不建议明文存密码
按摘要描述,系统支持管理员填写信息注册账号。最直接的做法是往AdminUser表插一条记录,但如果在真实场景里这么做,数据库一旦泄露,所有密码都明文暴露。哪怕这是教学项目,也建议至少做一次哈希处理。常见做法是使用MD5或者SHA256加盐,但MD5已被认为不安全,我一般偏好SHA256。
下面给出一个基于SHA256的密码哈希工具类写法:
public static class PasswordHelper { public static string HashPassword(string password, string salt) { using (var sha256 = System.Security.Cryptography.SHA256.Create()) { byte[] bytes = Encoding.UTF8.GetBytes(password + salt); byte[] hash = sha256.ComputeHash(bytes); StringBuilder sb = new StringBuilder(); foreach (byte b in hash) sb.Append(b.ToString("x2")); return sb.ToString(); } } }这段代码把明文密码和盐拼接后做SHA256哈希,输出64位十六进制字符串。盐可以用Guid.NewGuid().ToString()生成,存入用户表的一个单独字段。注册时先随机生成盐,再存哈希值;登录验证时用同一套规则计算后比较。顺带说明,很多老项目用的是MD5,如果现有数据库已有MD5密码,改造时要注意兼容逻辑,比如判断哈希长度来区分新旧算法。
3.2 登录状态怎么管理:全局类与窗体重定向
WinForms没有现成的Session机制,通常做法是建一个静态类保存当前登录用户信息。登录成功后把用户ID和用户名存进去,需要鉴权的窗体在加载时检查这个状态,为空就跳回登录页。
public static class UserSession { public static int CurrentUserId { get; set; } public static string CurrentUserName { get; set; } public static bool IsLoggedIn { get { return CurrentUserId > 0; } } }登录按钮的事件里,先调BLL层的登录方法验证账号密码,成功后再赋值UserSession,之后打开主窗体。这里有常见坑:如果登录成功但没给CurrentUserId赋值,主窗体的加载事件里判断IsLoggedIn会得到false,导致莫名其妙被踢回登录页。很多初学者遇到这种情况时还以为是自己SQL写错了,实际只是状态没传递。
3.3 密码修改的参数化SQL写法
修改密码在DAL层对应一条Update语句,关键点在于使用参数化查询,而不是拼接字符串。直接拼SQL不仅容易被注入,而且带单引号的密码会让语句直接报错。
public bool ChangePassword(int userId, string oldPwd, string newPwd) { string sql = "UPDATE AdminUser SET Password=@NewPwd WHERE UserId=@UserId AND Password=@OldPwd"; SqlParameter[] paras = new SqlParameter[] { new SqlParameter("@NewPwd", newPwd), new SqlParameter("@UserId", userId), new SqlParameter("@OldPwd", oldPwd) }; return SqlHelper.ExecuteNonQuery(sql, paras) > 0; }这里执行影响行数大于0表示密码修改成功。有个很小的细节容易忽略:返回结果是0不代表SQL报错,而是条件没匹配上,最常见原因是旧密码不对。所以界面层要把这个方法返回的bool直接提示给用户:“旧密码错误”或“修改成功”。如果你发现旧密码明明对但总是修改失败,先去确认数据库里存的密码格式是否做了哈希处理,如果注册时哈希了但修改时明文比较,必然失败。
3.4 登录模块的避坑清单
登录这块我踩过的坑不少,整理几条最实际的:
现象1:登录按钮点了没反应,调试没报错。原因:BLL层的登录方法返回了结果,但UI层没把结果对应到界面提示。解决:先检查DAL方法的返回值,用断点看返回的是true还是false,再顺着BLL往UI排查。现象2:注册成功但登录不上。原因:注册时密码字段和登录时验证字段长度不一致,或者密码加了盐但登录验证没做同样处理。解决:把注册和登录的密码处理逻辑放到同一个公共方法里,确保两端一致。现象3:连接数据库超时。原因:连接字符串里的Connect Timeout太小,或者SQL Server服务没启动。解决:在连接串里加Connect Timeout=10,保底先确认SSMS能连上。
4. 器材信息管理:增删查改的每个操作都有它的边界条件
4.1 添加器材:库存和必填字段的校验顺序
添加器材信息时,界面层最常犯的错误是只做了窗体上的输入框非空校验,没做业务层校验。比如库存值填了负数,或者器材名称带空格直接入库。合格的做法是UI层做基础必填提示,BLL层做业务规则校验,DAL层负责最终落库。
public bool AddEquipment(Equipment entity) { if (string.IsNullOrWhiteSpace(entity.Name)) throw new ArgumentException("器材名称不能为空"); if (entity.Stock < 0) throw new ArgumentException("库存数量不能为负数"); if (string.IsNullOrWhiteSpace(entity.Type)) entity.Type = "未分类"; string sql = "INSERT INTO Equipment (Name, Type, Stock, Price) VALUES (@Name, @Type, @Stock, @Price)"; SqlParameter[] paras = new SqlParameter[] { new SqlParameter("@Name", entity.Name), new SqlParameter("@Type", entity.Type), new SqlParameter("@Stock", entity.Stock), new SqlParameter("@Price", entity.Price) }; return SqlHelper.ExecuteNonQuery(sql, paras) > 0; }这段代码里的两个if很重要:名称和库存是业务底线,直接抛异常让调用方捕获并提示,比静默返回false更好排查。另一个细节是Type为空时给默认值“未分类”,否则后续按器材种类查询时,会出现一堆空字符串分组,前端下拉框看起来非常难看。Price字段要注意类型,SQL Server里用decimal(10,2),实体类对应decimal,如果界面层把文本框内容转成decimal格式不对,会在这层就抛FormatException。
4.2 删除器材:物理删还是逻辑删
摘要明确写了“对器材信息从数据库移除”,说明原始项目做的是物理删除,也就是DELETE FROM Equipment WHERE EquipmentId=@Id。但我建议你在这个位置多想一步:如果器材表有借还记录关联,物理删除会导致历史记录变成孤儿数据,查询借还报表时关联不到器材名称。常见做法是增加一个IsDeleted字段,删除时执行Update把标记置1,查询时统一过滤。
public bool DeleteEquipment(int equipmentId) { // 方案一:物理删除 // string sql = "DELETE FROM Equipment WHERE EquipmentId=@Id"; // 方案二:逻辑删除 string sql = "UPDATE Equipment SET IsDeleted=1 WHERE EquipmentId=@Id"; return SqlHelper.ExecuteNonQuery(sql, new SqlParameter("@Id", equipmentId)) > 0; }实际项目里我更倾向方案二,尤其是器材可能被借还记录引用时。但要注意,逻辑删除后所有查询列表的地方都要带WHERE IsDeleted=0,漏掉任何一处,删除的器材又会在列表里冒出来,这种bug排查起来很费劲。另外库存统计时也要过滤Deleted数据,不然总数会虚高。
4.3 查找器材:精确查询与模糊查询的组合条件拼法
按摘要描述,查找功能支持“器材编号精确查询”“器材名模糊查询”“按器材种类查询”和“库存排序显示”。这四个场景本质上是同一个方法,根据不同条件动态拼接SQL。
public List<Equipment> QueryEquipment(string id, string name, string type, string sortOrder) { List<string> conditions = new List<string>(); List<SqlParameter> paras = new List<SqlParameter>(); string sql = "SELECT * FROM Equipment WHERE IsDeleted=0"; if (!string.IsNullOrWhiteSpace(id)) { conditions.Add("EquipmentId=@Id"); paras.Add(new SqlParameter("@Id", id)); } if (!string.IsNullOrWhiteSpace(name)) { conditions.Add("Name LIKE @Name"); paras.Add(new SqlParameter("@Name", "%" + name + "%")); } if (!string.IsNullOrWhiteSpace(type)) { conditions.Add("Type=@Type"); paras.Add(new SqlParameter("@Type", type)); } if (conditions.Count > 0) sql += " AND " + string.Join(" AND ", conditions); if (sortOrder == "stock_asc") sql += " ORDER BY Stock ASC"; else if (sortOrder == "stock_desc") sql += " ORDER BY Stock DESC"; else sql += " ORDER BY EquipmentId"; return SqlHelper.QueryList<Equipment>(sql, paras.ToArray()); }这里值得多说一句:模糊查询参数化时,通配符%是加在参数值里,而不是拼在SQL字符串里。很多初学者写成WHERE Name LIKE '%@Name%',执行后查不到任何数据,因为@Name被当成字符串参与匹配,而不是被替换成实际值。另一个细节是排序字段白名单化——sortOrder不要直接拼接进SQL,否则外部传入恶意字段名会导致SQL注入;上面代码用if分支穷举合法排序方式,属于防御性写法。
4.4 修改器材信息:核对受影响字段
修改器材信息时,界面上通常会把已有数据呈现出来,用户改了哪些字段就更新哪些。最简单的方式是全字段更新,但要注意一个问题:如果回填时实体的某个字段是空值,全字段更新会把数据库里的原有值覆盖成空。比如器材价格没在界面上展示,实体Price字段为默认0,更新后价格就变成0了。对这类情况,要么改成只更新界面涉及的字段,要么在更新前先查一次库,把未修改的字段补全再提交。
public bool UpdateEquipment(Equipment entity) { string sql = "UPDATE Equipment SET Name=@Name, Type=@Type, Stock=@Stock, Price=@Price WHERE EquipmentId=@Id"; SqlParameter[] paras = new SqlParameter[] { new SqlParameter("@Name", entity.Name), new SqlParameter("@Type", entity.Type), new SqlParameter("@Stock", entity.Stock), new SqlParameter("@Price", entity.Price), new SqlParameter("@Id", entity.EquipmentId) }; return SqlHelper.ExecuteNonQuery(sql, paras) > 0; }更新操作通常返回影响行数,如果返回0,优先怀疑EquipmentId不存在或者界面上根本没把主键传到实体里。还有个隐性问题:在某些项目里主键字段叫Id,实体类属性叫EquipmentId,手写SQL时别名对不上,执行会直接报“列名无效”。代码规范的好处在这里体现出来了——实体字段和数据库字段保持同名,能减少一批低级错误。
5. 借还器材登记:库存更新的常见坑与事务处理
5.1 借出流程:先看库存再加判断
借出器材的代码逻辑上是个两步骤操作:检查库存是否大于0,再执行更新减库存。看起来简单,但两个步骤之间如果并发来了另一个借出请求,库存就会被减成负数。在单用户教学系统里这个问题不明显,但作为工程习惯,应该把判断和更新合并到一条SQL里,或者包在数据库事务里。
合并写法如下:
public bool BorrowEquipment(int equipmentId, int quantity, string borrower) { string sql = @"UPDATE Equipment SET Stock = Stock - @Qty WHERE EquipmentId=@Id AND Stock >= @Qty"; int rows = SqlHelper.ExecuteNonQuery(sql, new SqlParameter("@Qty", quantity), new SqlParameter("@Id", equipmentId)); if (rows > 0) { string insertSql = "INSERT INTO BorrowRecord (EquipmentId, Borrower, BorrowDate, Quantity) VALUES (@Id, @Borrower, GETDATE(), @Qty)"; SqlHelper.ExecuteNonQuery(insertSql, new SqlParameter("@Id", equipmentId), new SqlParameter("@Borrower", borrower), new SqlParameter("@Qty", quantity)); return true; } return false; }WHERE Stock >= @Qty这条件把库存检查直接放进Update里,不满足条件时影响行数为0,从根上避免超借。如果你用的是先查后改,可以再加事务包住:SqlTransaction开启,查询库存,判断足够后Update,然后Insert记录,最后Commit。注意一定要在try-catch里执行Rollback,否则一个步骤失败另一步却成功了,数据就彻底对不上。
5.2 归还流程:更新库存与回填归还日期
归还是借出的逆操作,核心是找到对应的借出记录,回填ReturnDate,并同步把库存加回去。这里有个逻辑细节:是允许同一器材被同一个人分多次借出,每次归还都加对应数量,还是严格控制一本一还?这个系统从描述看走的是简化逻辑——归还时根据借还记录主键找到未归还记录,更新归还日期,再更新器材库存。
public bool ReturnEquipment(int recordId, int equipmentId, int quantity) { string updateRecordSql = "UPDATE BorrowRecord SET ReturnDate=GETDATE() WHERE RecordId=@RecordId AND ReturnDate IS NULL"; int rows = SqlHelper.ExecuteNonQuery(updateRecordSql, new SqlParameter("@RecordId", recordId)); if (rows == 0) return false; string updateStockSql = "UPDATE Equipment SET Stock=Stock+@Qty WHERE EquipmentId=@Id"; SqlHelper.ExecuteNonQuery(updateStockSql, new SqlParameter("@Qty", quantity), new SqlParameter("@Id", equipmentId)); return true; }这里的硬性约束是ReturnDate IS NULL,防止同一条记录被重复归还导致库存被多次加回来。如果你在这个判断上偷懒,只按RecordId更新,那么用户双击两次归还按钮,库存就会多加一倍。界面层最好在归还成功后就刷新当前行的状态,按钮也置灰,双保险。
5.3 借还记录的查询显示:连表查询比单表更实用
借还记录如果只存EquipmentId,列表页显示时用户看到一列数字编号,不知道具体是什么器材。实际做法是连表查询,把器材名称、型号等信息一起查出来。
SELECT r.RecordId, e.Name AS EquipmentName, r.Borrower, r.BorrowDate, r.ReturnDate, r.Quantity FROM BorrowRecord r INNER JOIN Equipment e ON r.EquipmentId = e.EquipmentId ORDER BY r.BorrowDate DESC在DAL层,这段SQL用SqlDataAdapter填充DataTable,直接绑定到DataGridView即可。需要注意,如果你是使用实体泛型映射的方式写DAL层,连表查询返回的列和实体字段不一致时,要么定义一个新的视图模型类,要么直接用DataTable。这个选择看项目里SqlHelper封装到什么程度,如果封装了QueryList<T>,就直接用BorrowRecordViewModel实体去接。
5.4 借还模块中让人头疼的并发和状态问题
现象:两台电脑同时操作同一器材的借出,系统都提示成功,库存却变负数。原因:代码是“先查询再更新”,两个请求都读到库存为1,然后各自执行减1,结果库存变成-1。解决:用前面提到的UPDATE ... WHERE Stock >= @Qty原子操作,或者给器材表加锁提示,比如WITH (UPDLOCK)强制更新锁。现象:归还时明明有记录却提示失败。原因:ReturnDate已经被写入,说明之前已经归还过,界面没有禁用二次操作。解决:查询列表时就根据ReturnDate IS NULL区分状态,已归还记录直接隐藏归还按钮。
6. 项目改造与常见坑位排查:让BSM跑得更顺手
6.1 最常见的翻车点:SQL Server版本与登录模式
这个项目默认面向SQL Server,不同机器的认证模式不一样。我见过不少人在本机用Windows身份验证装好SQL Server,拿到的项目连接串里却写着User ID=sa,怎么都连不上。这时不要盲目去设sa密码,直接改连接串最省事:Data Source=.;Initial Catalog=BSMDB;Integrated Security=True。另外还要确认数据库文件是直接附加还是通过脚本建库,如果是.mdf文件,直接在SSMS里附加,注意数据文件路径不能放在中文目录或有特殊字符的路径下,否则附加会报路径错误。
6.2 数据访问层的封装:SqlHelper用着顺手但别忽略资源释放
绝大多数C#三层架构项目里都有个SqlHelper静态类,封装了执行增删查改的通用方法。这个类内部一般使用SqlConnection、SqlCommand、SqlDataReader,代码里必须遵守用完即释放的原则。如果你不想每次写using,至少把Close()放到finally里。
public static int ExecuteNonQuery(string sql, params SqlParameter[] paras) { using (SqlConnection conn = new SqlConnection(GetConnStr())) using (SqlCommand cmd = new SqlCommand(sql, conn)) { if (paras != null) cmd.Parameters.AddRange(paras); conn.Open(); return cmd.ExecuteNonQuery(); } }using语法保证连接在方法结束时自动关闭,即使执行过程抛异常也会被处理。如果发现程序运行一段时间后报“连接池已满”,十有八九是某个地方手写了SqlConnection但没Close,排查时全局搜索new SqlConnection,看每个分支是否都在finally或using块里。另一个性能细节:查出的数据只读一次就别用DataAdapter,直接用SqlDataReader流式读取,内存占用会小很多。
6.3 DataGridView数据显示:列顺序和编辑状态的坑
器材列表用DataGridView绑定后,默认会按查询结果顺序显示所有列,包括EquipmentId这种用户不太关心的主键列。我处理这类界面的习惯是在窗体加载时手动设置列标题和可见性:
dataGridView1.AutoGenerateColumns = false; dataGridView1.Columns.Clear(); dataGridView1.Columns.Add("EquipmentId", "编号"); dataGridView1.Columns.Add("Name", "器材名称"); dataGridView1.Columns.Add("Type", "种类"); dataGridView1.Columns.Add("Stock", "库存"); dataGridView1.Columns.Add("Price", "价格"); dataGridView1.DataSource = list;这里有个常见翻车点:设置AutoGenerateColumns=false之后,如果你忘记把DataPropertyName设置到对应实体属性,单元格会全部显示空。正确写法是建列时指定:dataGridView1.Columns["Name"].DataPropertyName = "Name"。另外,绑定列表后不要直接改单元格内容触发编辑状态,先调用dataGridView1.ClearSelection()取消选中,否则删除操作时会遇到“当前单元格正在编辑无法删除”的报错。这个报错看似很奇怪,实际就是编辑状态没退出。
6.4 模糊查询变成全表扫的优化思路
器材数量少的时候,LIKE '%关键词%'没任何问题;等数据量上了十万,前置通配符的模糊查询会让索引失效,全表扫描。这个系统的定位决定了不会有大数据的压力,但作为工程习惯,可以给常用查询建立索引:CREATE INDEX IX_Equipment_Name ON Equipment(Name)。注意普通索引对前置通配符依然无效,如果你真能把查询条件改成LIKE '关键词%',才能利用索引加速。对这个项目来说,最具性价比的方案是在SqlHelper里加一个SQL执行耗时日志,把超过500ms的查询语句打出来,针对慢查询做分析。
6.5 主键自增与删除后的ID间隙问题
器材表的主键通常设成IDENTITY自增,删除靠后的记录后,新插入记录的ID会继续递增,表格里出现跳号。如果需求要求编号连续,就得在插入前查当前最大ID再加1,但这在高并发下会冲突。我自己的取舍是分场景:这个系统里器材编号是业务标识,用户可能根据编号做精确查询,所以保持自增没问题;如果是订单编号或者票据编号,则不应该用IDENTITY,应该在业务层生成编号。理解了这层区别,你改造项目时就不会一到ID规则就抓瞎。跳号本身不算bug,只要查询逻辑不依赖ID连续性,没必要专门处理。
6.6 验证功能是否完整的自查清单
跑完这套系统,建议按下面列表逐项过一遍,确认每个功能都真的可用:
| 功能模块 | 验证方式 | 预期结果 |
|---|---|---|
| 注册 | 新账号注册成功后登出再登录 | 能正常登录 |
| 密码修改 | 旧密码正确时修改,再用新密码登录 | 登录成功 |
| 添加器材 | 必填项缺省时提交 | 提示错误,不插入数据 |
| 删除器材 | 删除后列表刷新 | 列表不再显示该记录 |
| 精确/模糊查询 | 输入编号精确查,输入关键字模糊查 | 返回正确结果 |
| 库存排序 | 分别按升序和降序查看 | 排序方向正确 |
| 借出 | 对库存为0的器材执行借出 | 提示失败,库存不变 |
| 归还 | 归还后再执行归还 | 第二次操作被拒绝 |
这套验证流程每次改完代码都要走一遍,花不了十分钟,但能挡住很多改一处崩一处的连锁问题。我自己有个习惯,每次改完DAL层或者BLL层之后,会把查询、新增、修改、删除四类操作各执行一次,确认没有隐性影响再继续下一块,这个习惯支撑我拆过好几个类似的教学项目,从来没有交付出明显翻车的版本。希望这份拆解对你有用,照着跑通一遍,你会对C#三层架构有更实际的体感。
本文还有配套的精品资源,点击获取