news 2026/10/13 1:35:45

C# WinForm医院药品管理系统源码解析:SQL Server数据库设计与事务处理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C# WinForm医院药品管理系统源码解析:SQL Server数据库设计与事务处理

简介:这是一套面向高校计算机相关专业学生与初学者的医院药品管理系统完整源码,基于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 List<DrugStock> GetNearExpireDrugs(int days) { var list = new List<DrugStock>(); 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'阿莫西林胶囊', N'0.25g*24粒', N'盒', N'某制药厂', 20), ('10002', N'布洛芬缓释胶囊', N'0.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 name="DrugConn" connectionString="Data Source=.;Initial Catalog=HospitalDrugDB;Integrated Security=True" providerName="System.Data.SqlClient"/> </connectionStrings> </configuration>

Data Source=.表示本机默认实例,如果你装的是命名实例就写.\SQLEXPRESS。Integrated Security=True用 Windows 身份验证,省得配账号密码;如果医院要求 SQL 账号登录,就换成User ID=sa;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前缀。解决方法是所有存中文的字段一律NVARCHAR,C# 里用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 Framework

WinForm 项目依赖 .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表,记录当前库结构版本号,程序启动时检查版本,不匹配就提示「请先执行升级脚本」。这个习惯是从一次血泪经验来的——有次给客户升级程序忘了同步改库,程序直接崩在登录界面,查了半天才发现是少了个字段。从那以后,库版本校验成了我每个项目的标配。希望这些能帮到你,少走点弯路。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 1:35:04

从zhilian1.zip到可运行项目:解压、环境搭建与验证指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:32:39

树莓派与Ubuntu上MAVROS安装全攻略:从环境配置到定点降落实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:31:46

冒泡排序深度优化:面向嵌入式与实时系统的可控性重构

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:30:42

MinGW-w64离线安装包部署全攻略:从选型到环境变量与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华