简介这是一套面向高校计算机相关专业学生与初学者的医院药品管理系统完整源码基于C#的WinForm框架与SQL Server数据库开发适用于课程设计、毕业设计或二次开发参考。系统覆盖用户管理、药品信息维护、入库出库、药品申领与审核、库存预警及预警数量设置、病人信息录入等模块能帮助读者理解桌面端业务系统的分层设计与数据库交互流程。压缩包共154个文件约7.39MB以52个cs源码文件为核心辅以resx与resources界面资源、dll依赖库、json与cache等配置缓存文件并附带mdf与ldf数据库文件便于直接还原运行环境。目前已有226人学习下载。整体界面美观、功能完整读者可据此掌握WinForm窗体布局、SQL Server数据表设计以及申领审核、库存预警等业务逻辑的实现思路是快速搭建同类管理系统的实用参考。1. 从一份 WinForm 药品管理源码说起它到底能解决医院药房的哪些真问题药房窗口前排着队药师一边翻纸质台账一边找批号临近过期的药品压在货架最里层没人发现——这是很多中小医院和社区门诊的真实日常。基于 C# 的医院药品管理系统WinForm 源码 SQL Server 数据库就是冲着这类场景来的用一套桌面客户端把药品入库、出库、库存预警、处方发药和效期管理串起来数据落在 SQL Server 里前端用 WinForm 做交互。它适合两类人一类是刚学完 C# 基础、想找一个完整项目练手的开发者另一类是需要给小型医疗机构快速搭一套可用系统的实施人员。这套东西的价值不在于技术多新而在于它把「药品」这个强合规、强流程的业务跑通了你能从中学到 WinForm 项目分层、SQL Server 表设计和事务处理这些实打实的东西。下面我按自己复现这类项目的顺序把选型、建库、编码和踩坑一条条讲清楚。2. 技术选型与数据库设计为什么是 WinForm SQL Server 而不是别的2.1 WinForm 在这个场景里为什么还没被淘汰很多人一听 WinForm 就觉得老但放到医院药房这种内网、单机或局域网、操作人员固定的环境里它反而是最省事的选择。药房工作站通常就几台机器网络稳定不需要考虑跨平台和浏览器兼容WinForm 拖控件就能出界面部署时打个安装包双击就完事。相比之下Web 方案要配服务器、处理会话、考虑浏览器差异对一个小药房来说纯属过度设计。从开发效率看WinForm 的数据绑定能力配合DataGridView能快速做出药品列表、库存明细这类表格界面BindingSource一挂增删改查的骨架就出来了。热搜里常出现的「winform做简单表格」「winform界面美化」其实都指向同一个诉求把数据展示清楚、操作顺手。我的建议是界面别追求花哨药房系统第一要务是信息密度和操作速度药师扫一眼就能看到批号、效期、库存量比什么渐变按钮都重要。选型上还要注意 .NET 版本。老项目常见的是 .NET Framework 4.5 到 4.8配合 VS2015 或更高版本打开。如果你用 VS2022 打开老项目可能会提示重定向框架版本这时候在项目属性里把目标框架改成你机器上装了的版本即可别硬扛着不升级否则某些 NuGet 包会装不上。2.2 SQL Server 表结构怎么设计才扛得住真实业务药品管理系统的核心表其实不多但每张表的字段设计直接决定后面写代码顺不顺。下面是我一般会用的最小可用表结构你可以照着建也可以在此基础上加字段。-- 药品基础信息表 CREATE TABLE Drug ( DrugID INT IDENTITY(1,1) PRIMARY KEY, DrugCode VARCHAR(20) NOT NULL UNIQUE, -- 药品编码业务唯一 DrugName NVARCHAR(100) NOT NULL, -- 药品名称 Spec NVARCHAR(50), -- 规格 Unit NVARCHAR(10), -- 单位 Manufacturer NVARCHAR(100), -- 生产厂家 IsPrescription BIT DEFAULT 0, -- 是否处方药 WarnStock INT DEFAULT 10, -- 库存预警下限 CreateTime DATETIME DEFAULT GETDATE() ); -- 药品批次库存表同一药品多批次 CREATE TABLE DrugStock ( StockID INT IDENTITY(1,1) PRIMARY KEY, DrugID INT NOT NULL, BatchNo VARCHAR(30) NOT NULL, -- 批号 ProduceDate DATE, ExpireDate DATE NOT NULL, -- 有效期预警靠它 Quantity INT NOT NULL DEFAULT 0, FOREIGN KEY (DrugID) REFERENCES Drug(DrugID) ); -- 出入库流水表 CREATE TABLE StockFlow ( FlowID INT IDENTITY(1,1) PRIMARY KEY, DrugID INT NOT NULL, BatchNo VARCHAR(30), FlowType TINYINT NOT NULL, -- 1入库 2出库 Quantity INT NOT NULL, OperUser NVARCHAR(50), FlowTime DATETIME DEFAULT GETDATE() );这里有几个设计要点值得说。第一药品和库存分成两张表因为同一种药会有多个批号每个批号有效期不同混在一起就没法做效期预警。第二StockFlow流水表只增不改任何库存变动都留痕这是药品管理的合规底线出了问题能倒查。第三ExpireDate用DATE类型而不是字符串后面做「近效期查询」时可以直接用日期函数比较省得做字符串转换。热搜里「sqlserver 字符串转数字」「sqlserver多行合并成一行」这类问题在这个项目里都会遇到。比如药品编码如果设计成字符串但你想按数字排序就得用CAST(DrugCode AS INT)但前提是编码里不能有字母否则转换直接报错。我的做法是编码统一用定长数字串排序时再转避免踩这个坑。2.3 项目分层别把所有代码堆在 Form 里新手最容易犯的错是把 SQL 语句、业务判断、界面刷新全写在按钮的 Click 事件里一个 Form 文件几千行改一处崩三处。我一般会分三层DAL层管数据库访问BLL层管业务规则比如出库时校验库存够不够UI层只管界面。这样后面加个「处方审核」功能只动 BLL界面不用大改。// DAL 层示例查询近效期药品 public ListDrugStock GetNearExpireDrugs(int days) { var list new ListDrugStock(); string sql SELECT s.*, d.DrugName FROM DrugStock s JOIN Drug d ON s.DrugID d.DrugID WHERE s.ExpireDate DATEADD(DAY, Days, GETDATE()) AND s.Quantity 0 ORDER BY s.ExpireDate; using (var conn new SqlConnection(connStr)) using (var cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(Days, days); conn.Open(); using (var reader cmd.ExecuteReader()) { while (reader.Read()) { list.Add(new DrugStock { StockID (int)reader[StockID], DrugName reader[DrugName].ToString(), BatchNo reader[BatchNo].ToString(), ExpireDate (DateTime)reader[ExpireDate], Quantity (int)reader[Quantity] }); } } } return list; }这段代码的关键点是用了参数化查询Days而不是字符串拼接。药品系统里用户输入频繁拼接 SQL 迟早出注入问题。另外using保证连接和命令对象及时释放药房系统长时间开着连接泄漏会导致后面越来越卡。参数days一般设 30 或 60代表提前多少天预警这个值建议做成配置项不同医院要求不一样。3. 从零把系统跑起来建库、连库、写第一个可用界面3.1 SQL Server 安装与数据库初始化拿到源码包后第一步不是急着打开解决方案而是先把数据库环境弄好。SQL Server 版本选择上开发机装 Developer 版就够生产环境按医院要求来。安装时如果遇到「无法找到数据库引擎启动句柄」这类报错八成是之前装过残留没清干净去控制面板卸载所有 SQL Server 相关组件重启后再装别反复点重试。装完后用 SSMS 连上实例新建一个数据库比如叫HospitalDrugDB然后把上一节的建表语句执行一遍。如果你拿到的是带.bak备份文件的源码包直接在 SSMS 里右键「数据库」→「还原数据库」选设备找到 bak 文件还原即可比手动建表快。-- 初始化一些基础数据方便测试 INSERT INTO Drug (DrugCode, DrugName, Spec, Unit, Manufacturer, WarnStock) VALUES (10001, N阿莫西林胶囊, N0.25g*24粒, N盒, N某制药厂, 20), (10002, N布洛芬缓释胶囊, N0.3g*20粒, N盒, N某制药厂, 15); INSERT INTO DrugStock (DrugID, BatchNo, ProduceDate, ExpireDate, Quantity) VALUES (1, 20240101, 2024-01-01, 2026-01-01, 100), (2, 20240315, 2024-03-15, 2025-09-15, 50);执行完这两条你就有测试数据了。注意N前缀中文必须加否则存进去是乱码这是 SQL Server 的老规矩新手经常忘。3.2 连接字符串配置与 WinForm 主界面搭建数据库好了接下来在 WinForm 项目里配连接字符串。我一般放在App.config里而不是硬编码在代码里这样换环境只改配置文件。configuration connectionStrings add nameDrugConn connectionStringData Source.;Initial CatalogHospitalDrugDB;Integrated SecurityTrue providerNameSystem.Data.SqlClient/ /connectionStrings /configurationData Source.表示本机默认实例如果你装的是命名实例就写.\SQLEXPRESS。Integrated SecurityTrue用 Windows 身份验证省得配账号密码如果医院要求 SQL 账号登录就换成User IDsa;Password你的密码。读连接字符串用ConfigurationManager.ConnectionStrings[DrugConn].ConnectionString记得引System.Configuration程序集。主界面我一般用一个TabControl分几个页签药品管理、入库、出库、库存查询、效期预警。每个页签里放DataGridView加几个操作按钮。药品列表的加载逻辑就是调 DAL 层查询绑到BindingSource上。private void LoadDrugList() { var dt drugDal.GetAllDrugs(); // 返回 DataTable bindingSource1.DataSource dt; dataGridView1.DataSource bindingSource1; // 隐藏不需要展示的列 if (dataGridView1.Columns[DrugID] ! null) dataGridView1.Columns[DrugID].Visible false; }这里BindingSource的好处是后面做筛选、排序不用重新查库直接设bindingSource1.Filter就行。DataGridView列宽和显示格式可以在设计器里调也可以在代码里设AutoSizeColumnsMode Fill让列自动铺满省得手动拖。3.3 入库出库的事务处理库存不能算错药品出入库必须用事务否则出现「流水记了但库存没加」这种脏数据对账时能让人崩溃。下面是一个入库的完整逻辑先插流水再更新库存两步在一个事务里。public bool StockIn(int drugId, string batchNo, DateTime expireDate, int qty, string user) { using (var conn new SqlConnection(connStr)) { conn.Open(); using (var tran conn.BeginTransaction()) { try { // 1. 写流水 string flowSql INSERT INTO StockFlow(DrugID,BatchNo,FlowType,Quantity,OperUser) VALUES(DrugID,BatchNo,1,Qty,User); using (var cmd new SqlCommand(flowSql, conn, tran)) { cmd.Parameters.AddWithValue(DrugID, drugId); cmd.Parameters.AddWithValue(BatchNo, batchNo); cmd.Parameters.AddWithValue(Qty, qty); cmd.Parameters.AddWithValue(User, user); cmd.ExecuteNonQuery(); } // 2. 更新库存同批号累加没有则插入 string stockSql MERGE DrugStock AS t USING (SELECT DrugID AS DrugID, BatchNo AS BatchNo) AS s ON t.DrugID s.DrugID AND t.BatchNo s.BatchNo WHEN MATCHED THEN UPDATE SET Quantity t.Quantity Qty WHEN NOT MATCHED THEN INSERT(DrugID,BatchNo,ExpireDate,Quantity) VALUES(DrugID,BatchNo,ExpireDate,Qty);; using (var cmd new SqlCommand(stockSql, conn, tran)) { cmd.Parameters.AddWithValue(DrugID, drugId); cmd.Parameters.AddWithValue(BatchNo, batchNo); cmd.Parameters.AddWithValue(ExpireDate, expireDate); cmd.Parameters.AddWithValue(Qty, qty); cmd.ExecuteNonQuery(); } tran.Commit(); return true; } catch (Exception ex) { tran.Rollback(); // 记录日志别直接 MessageBox 弹异常堆栈给用户 LogHelper.Write(ex); return false; } } } }MERGE语句在这里很合适一条语句搞定「有则更新无则插入」比先查再判断少一次往返。参数qty入库时是正数出库时传负数或者单独写一个出库方法把FlowType设成 2、库存做减法我倾向于分开写逻辑更清晰。事务里任何一步抛异常都Rollback保证流水和库存一致。4. 避坑与排查这类项目最容易翻车的五个地方4.1 现象程序在自己电脑能跑换台机器就报连接失败原因基本是连接字符串写死了Data Source.而目标机器装的是命名实例或者根本没装 SQL Server。解决方法是把连接字符串抽到配置文件部署时让实施人员改更稳妥的做法是做一个「数据库配置」小窗口首次运行让用户填服务器地址和库名存到本地配置里。4.2 现象中文药品名存进数据库变成问号原因是建表时字段用了VARCHAR而不是NVARCHAR或者插入时参数没加N前缀。解决方法是所有存中文的字段一律NVARCHARC# 里用SqlParameter传值本身没问题但如果你拼 SQL 字符串记得写成N阿莫西林。已经存错的只能清库重来没有后悔药。4.3 现象库存数量和流水对不上差几个多半是出库时没校验库存或者并发操作时两个窗口同时扣减。解决方法是出库前先SELECT Quantity判断够不够更新时用UPDATE DrugStock SET Quantity Quantity - Qty WHERE StockID Id AND Quantity Qty靠WHERE条件保证不会扣成负数再检查ExecuteNonQuery返回的影响行数为 0 就说明库存不足回滚事务。4.4 现象DataGridView 数据量大时界面卡死药品流水几万条一次性查出来绑上去界面直接假死。解决方法是分页查询SQL Server 用OFFSET ... FETCH NEXT做分页每页 50 条。热搜里「sqlserver offset 后再 top 20 查到的是什么」就是分页写错了顺序OFFSET必须配合ORDER BY且FETCH NEXT在ORDER BY之后顺序错了语法直接报错。4.5 现象打包成安装程序后目标机器提示缺少 .NET FrameworkWinForm 项目依赖 .NET Framework 运行时目标机器没装就起不来。解决方法是在安装包项目里把 .NET Framework 作为前置条件勾上用 VS 自带的 Installer Projects 或第三方打包工具生成安装程序时把运行时一起带上。热搜里「winform打包成安装程序」问的就是这个别只打包 exe依赖项要一起处理。5. 进阶技巧用 SQL 批量操作和效期预警把系统做扎实系统能跑起来只是及格真正让药房愿意用下去的是效期预警和批量操作这两个功能。先说效期预警前面GetNearExpireDrugs已经查出了近效期药品界面上可以用DataGridView的CellFormatting事件把快过期的行标红药师一眼就能看到。private void dataGridView1_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e) { if (dataGridView1.Columns[e.ColumnIndex].Name ExpireDate e.Value ! null) { DateTime expire Convert.ToDateTime(e.Value); int daysLeft (expire - DateTime.Today).Days; if (daysLeft 30) e.CellStyle.BackColor Color.LightCoral; // 30天内标红 else if (daysLeft 90) e.CellStyle.BackColor Color.LightYellow; // 90天内标黄 } }这段代码在单元格绘制时动态判断不改数据本身性能也扛得住。阈值 30 和 90 可以按医院规定调整有的要求 3 个月预警有的要求 6 个月。再说批量入库。药房一次到货几十种药一条条录入效率太低。我一般会做一个 Excel 导入功能用SqlBulkCopy把整理好的数据一次性写进临时表再和正式表做比对合并。热搜里「c# sqlbulkcopy 表变动有影响」提醒的是如果目标表结构改了比如加了列SqlBulkCopy的列映射要同步改否则会报列不匹配。我的习惯是导入前先校验 Excel 列名和数据库列名是否一致不一致直接提示用户别等写到一半才报错。using (var bulk new SqlBulkCopy(connStr)) { bulk.DestinationTableName DrugStock; bulk.ColumnMappings.Add(DrugID, DrugID); bulk.ColumnMappings.Add(BatchNo, BatchNo); bulk.ColumnMappings.Add(ExpireDate, ExpireDate); bulk.ColumnMappings.Add(Quantity, Quantity); bulk.WriteToServer(dt); // dt 是整理好的 DataTable }ColumnMappings一定要显式写别依赖自动映射列顺序一变就错位。批量操作放在事务里失败整体回滚。最后说一个我自己的习惯每次改完数据库结构我都会在项目里维护一个db_version表记录当前库结构版本号程序启动时检查版本不匹配就提示「请先执行升级脚本」。这个习惯是从一次血泪经验来的——有次给客户升级程序忘了同步改库程序直接崩在登录界面查了半天才发现是少了个字段。从那以后库版本校验成了我每个项目的标配。希望这些能帮到你少走点弯路。本文还有配套的精品资源点击获取