ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C# WinForm药品管理系统源码:SQL Server数据库实现入库出库与库存预警

C# WinForm药品管理系统源码:SQL Server数据库实现入库出库与库存预警 简介这是一套面向高校计算机相关专业学生与C#初学者的医院药品管理系统完整源码包基于WinForm框架与SQL Server数据库开发可作为课程设计、毕业设计或二次开发参考。系统覆盖用户管理、药品信息维护、入库出库、药品申领与审核、库存预警及预警数量设置、病人信息录入等模块较完整地还原了医院药品流转的业务闭环。压缩包共154个文件约7.39MB以52个cs源码文件为核心辅以resx与resources资源文件、dll依赖库、json配置及mdf/ldf数据库文件并附带sln解决方案与csproj工程文件可直接用Visual Studio打开运行。目前已有226人学习下载。对于需要快速搭建桌面端管理系统的读者可从中参考WinForm界面布局、SQL Server数据表设计以及申领审核、库存预警等业务逻辑的实现思路节省从零搭建框架的时间。1. 从一份 WinForm 药品管理源码说起它到底能解决药房里的哪些麻烦药房里最怕的不是药多而是账对不上。入库、出库、盘点、效期预警、库存下限提醒这几件事只要有一件靠手工台账月底就得加班到怀疑人生。基于 C# 的医院药品管理系统WinForm 源码 SQL Server 数据库这个标题指向的就是一套典型的桌面端药房业务系统用 WinForm 做操作界面用 SQL Server 存药品、库存、出入库流水和用户权限最终打包成安装程序交付给药房或信息科。它适合两类人一类是计算机专业做课程设计或毕设的学生需要一套能跑通、能讲清业务闭环的完整案例另一类是中小医院、诊所或药企信息科的一线人员想用低成本方式把药品台账从 Excel 里搬出来。这套东西的核心价值不在界面多炫而在于把「药品基础信息 → 入库 → 出库 → 库存结余 → 效期预警」这条链路用数据库事务串起来让每一次库存变动都有流水可查。下面我按实际落地顺序把选型、建库、核心代码、打包和踩坑一次讲透。2. 技术选型与数据库设计为什么是 WinForm SQL Server 而不是别的2.1 WinForm 在这个场景里为什么还没被淘汰很多人一听 WinForm 就觉得老但在医院药房这种内网、单机或局域网、操作人员固定、不需要浏览器兼容的场景里WinForm 反而是最省事的选择。它启动快、控件拖拽即可布局、跟 SQL Server 的 ADO.NET 配合几乎零学习成本而且部署时只要装个 .NET Framework 运行时就能跑。对比 Web 方案WinForm 不需要额外配 IIS、不需要处理跨域和会话药房那台老电脑只要装了 SQL Server 客户端就能连库。常见做法是用 Visual Studio 2015 或更高版本建一个 Windows 窗体应用目标框架选 .NET Framework 4.5 以上这样兼容性最好。界面美化方面不用上第三方皮肤库把 DataGridView 的交替行颜色、列宽、冻结列调好再配几个 GroupBox 分区操作员就能看得清楚。热搜里常出现的 winform 界面美化、winform做简单表格本质上都是把 DataGridView 和布局容器用熟而不是堆控件。2.2 数据库表结构怎么定五张核心表撑起整个药房药品管理系统的数据库不需要几十张表抓住主数据、库存、流水、用户、供应商这五块就够。下面是我一般会用的建库脚本直接在 SQL Server Management Studio 里执行即可。注意数据库名和文件路径按自己机器改别照抄路径导致建库失败。-- 创建数据库路径按实际磁盘调整 CREATE DATABASE PharmacyDB ON PRIMARY ( NAME PharmacyDB_data, FILENAME D:\SQLData\PharmacyDB_data.mdf, SIZE 20MB, FILEGROWTH 5MB ) LOG ON ( NAME PharmacyDB_log, FILENAME D:\SQLData\PharmacyDB_log.ldf, SIZE 10MB, FILEGROWTH 5MB ); GO USE PharmacyDB; GO -- 药品基础信息表 CREATE TABLE Drug ( DrugID INT IDENTITY(1,1) PRIMARY KEY, DrugCode VARCHAR(20) NOT NULL UNIQUE, -- 药品编码唯一 DrugName NVARCHAR(50) NOT NULL, -- 药品名称 Spec NVARCHAR(30) NULL, -- 规格 Unit NVARCHAR(10) NULL, -- 单位 Producer NVARCHAR(50) NULL, -- 生产厂家 Price DECIMAL(10,2) NOT NULL DEFAULT 0, -- 单价 StockQty INT NOT NULL DEFAULT 0, -- 当前库存 MinStock INT NOT NULL DEFAULT 10, -- 库存下限 ExpireDate DATE NULL, -- 有效期 IsDeleted BIT NOT NULL DEFAULT 0 -- 软删除标记 ); GO -- 入库流水表 CREATE TABLE StockIn ( InID INT IDENTITY(1,1) PRIMARY KEY, DrugID INT NOT NULL, InQty INT NOT NULL, InPrice DECIMAL(10,2) NOT NULL, Supplier NVARCHAR(50) NULL, InDate DATETIME NOT NULL DEFAULT GETDATE(), Operator NVARCHAR(20) NULL, CONSTRAINT FK_StockIn_Drug FOREIGN KEY (DrugID) REFERENCES Drug(DrugID) ); GO -- 出库流水表 CREATE TABLE StockOut ( OutID INT IDENTITY(1,1) PRIMARY KEY, DrugID INT NOT NULL, OutQty INT NOT NULL, OutReason NVARCHAR(50) NULL, -- 领用科室或销售 OutDate DATETIME NOT NULL DEFAULT GETDATE(), Operator NVARCHAR(20) NULL, CONSTRAINT FK_StockOut_Drug FOREIGN KEY (DrugID) REFERENCES Drug(DrugID) ); GO -- 用户表 CREATE TABLE SysUser ( UserID INT IDENTITY(1,1) PRIMARY KEY, UserName NVARCHAR(20) NOT NULL UNIQUE, PwdHash VARCHAR(64) NOT NULL, -- 存 SHA256 哈希不存明文 RoleName NVARCHAR(20) NOT NULL DEFAULT operator ); GO这段脚本的逻辑很直白Drug 表存药品主数据和当前库存StockIn 和 StockOut 分别记录每一次入库和出库SysUser 管登录和权限。参数上要留意几个点DrugCode 加了 UNIQUE 约束防止重复录入同一药品StockQty 默认 0实际库存由出入库操作去加减ExpireDate 用 DATE 类型方便后面做效期预警查询IsDeleted 做软删除避免直接删药品导致流水表外键断裂。MinStock 是库存下限低于这个值就要在界面上标红提醒。热搜里 sqlserver 字符串转数字、sqlserver多行合并成一行 这类问题在这个系统里主要出现在报表统计和编码转换上后面章节会具体说。2.3 连接字符串与数据访问层怎么放WinForm 里不要在每个窗体里写连接字符串抽一个 SqlHelper 静态类出来所有窗体调它。连接字符串放在 App.config 里换数据库只改一处。!-- App.config -- configuration connectionStrings add namePharmacyConn connectionStringData Source.;Initial CatalogPharmacyDB;User IDsa;Password你的密码; providerNameSystem.Data.SqlClient/ /connectionStrings /configuration// SqlHelper.cs —— 统一数据访问入口 using System; using System.Configuration; using System.Data; using System.Data.SqlClient; public static class SqlHelper { // 从配置文件读取连接字符串避免硬编码 private static readonly string connStr ConfigurationManager.ConnectionStrings[PharmacyConn].ConnectionString; // 执行增删改返回受影响行数 public static int ExecuteNonQuery(string sql, params SqlParameter[] ps) { using (SqlConnection conn new SqlConnection(connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) { if (ps ! null) cmd.Parameters.AddRange(ps); conn.Open(); return cmd.ExecuteNonQuery(); } } // 执行查询返回 DataTable方便直接绑 DataGridView public static DataTable ExecuteQuery(string sql, params SqlParameter[] ps) { using (SqlConnection conn new SqlConnection(connStr)) using (SqlCommand cmd new SqlCommand(sql, conn)) using (SqlDataAdapter adapter new SqlDataAdapter(cmd)) { if (ps ! null) cmd.Parameters.AddRange(ps); DataTable dt new DataTable(); adapter.Fill(dt); return dt; } } }这里的关键是参数化查询所有用户输入都走 SqlParameter不要用字符串拼接否则药品名里带个单引号就能把 SQL 搞崩这也是最常见的注入隐患。ExecuteQuery 返回 DataTable直接赋给 DataGridView.DataSource 就能显示省去手动绑定的麻烦。连接字符串里的 Data Source 用点表示本机默认实例如果 SQL Server 装的是命名实例要写成.\SQLEXPRESS这种形式。热搜里 sqlserver配置管理器安装、sqlserver 2016 2017 2019安装失败-无法找到数据库引擎启动句柄 这类问题多半是实例名没写对或者服务没启动先在服务里确认 SQL Server 服务是运行状态再用 SSMS 连一次确认实例名。3. 核心业务代码入库、出库和库存联动怎么写才不出错3.1 入库操作一次事务里同时写流水和加库存入库不是简单插一条记录它要同时做两件事往 StockIn 插一条流水把 Drug 表的 StockQty 加上本次数量。这两步必须在一个事务里否则流水写了库存没加账就乱了。下面是我常用的写法。// 入库按钮事件 private void btnStockIn_Click(object sender, EventArgs e) { int drugId Convert.ToInt32(cboDrug.SelectedValue); int qty int.Parse(txtInQty.Text.Trim()); decimal price decimal.Parse(txtInPrice.Text.Trim()); string supplier txtSupplier.Text.Trim(); if (qty 0) { MessageBox.Show(入库数量必须大于 0); return; } using (SqlConnection conn new SqlConnection(SqlHelper.ConnStr)) { conn.Open(); using (SqlTransaction tran conn.BeginTransaction()) { try { // 1. 插入入库流水 string sqlIn INSERT INTO StockIn(DrugID,InQty,InPrice,Supplier,Operator) VALUES(DrugID,InQty,InPrice,Supplier,Operator); SqlCommand cmdIn new SqlCommand(sqlIn, conn, tran); cmdIn.Parameters.AddWithValue(DrugID, drugId); cmdIn.Parameters.AddWithValue(InQty, qty); cmdIn.Parameters.AddWithValue(InPrice, price); cmdIn.Parameters.AddWithValue(Supplier, supplier); cmdIn.Parameters.AddWithValue(Operator, CurrentUser.UserName); cmdIn.ExecuteNonQuery(); // 2. 更新库存 string sqlUpd UPDATE Drug SET StockQty StockQty Qty WHERE DrugID DrugID; SqlCommand cmdUpd new SqlCommand(sqlUpd, conn, tran); cmdUpd.Parameters.AddWithValue(Qty, qty); cmdUpd.Parameters.AddWithValue(DrugID, drugId); cmdUpd.ExecuteNonQuery(); tran.Commit(); MessageBox.Show(入库成功); LoadDrugList(); // 刷新列表 } catch (Exception ex) { tran.Rollback(); MessageBox.Show(入库失败 ex.Message); } } } }逻辑说明先开事务插流水和改库存都挂在同一个事务上任何一步抛异常就整体回滚保证账实一致。参数上Operator 取当前登录用户名方便追溯是谁操作的库存更新用StockQty StockQty Qty而不是先查再算再写避免并发下两个操作员同时入库导致覆盖。这里有个血泪经验如果不用事务网络一抖或者程序崩了流水和库存就对不上后面盘点能查到你想哭。3.2 出库操作先校验库存再扣减出库比入库多一道校验库存不够不能出。而且出库同样要事务先插 StockOut 流水再扣 Drug 库存扣减时还要防止扣成负数。// 出库按钮事件 private void btnStockOut_Click(object sender, EventArgs e) { int drugId Convert.ToInt32(cboDrug.SelectedValue); int qty int.Parse(txtOutQty.Text.Trim()); string reason txtOutReason.Text.Trim(); using (SqlConnection conn new SqlConnection(SqlHelper.ConnStr)) { conn.Open(); using (SqlTransaction tran conn.BeginTransaction()) { try { // 1. 校验库存是否充足带 UPDLOCK 防止并发超卖 string sqlCheck SELECT StockQty FROM Drug WITH (UPDLOCK) WHERE DrugID DrugID; SqlCommand cmdCheck new SqlCommand(sqlCheck, conn, tran); cmdCheck.Parameters.AddWithValue(DrugID, drugId); int stock Convert.ToInt32(cmdCheck.ExecuteScalar()); if (stock qty) { MessageBox.Show(库存不足当前库存 stock); tran.Rollback(); return; } // 2. 插入出库流水 string sqlOut INSERT INTO StockOut(DrugID,OutQty,OutReason,Operator) VALUES(DrugID,OutQty,OutReason,Operator); SqlCommand cmdOut new SqlCommand(sqlOut, conn, tran); cmdOut.Parameters.AddWithValue(DrugID, drugId); cmdOut.Parameters.AddWithValue(OutQty, qty); cmdOut.Parameters.AddWithValue(OutReason, reason); cmdOut.Parameters.AddWithValue(Operator, CurrentUser.UserName); cmdOut.ExecuteNonQuery(); // 3. 扣减库存 string sqlUpd UPDATE Drug SET StockQty StockQty - Qty WHERE DrugID DrugID; SqlCommand cmdUpd new SqlCommand(sqlUpd, conn, tran); cmdUpd.Parameters.AddWithValue(Qty, qty); cmdUpd.Parameters.AddWithValue(DrugID, drugId); cmdUpd.ExecuteNonQuery(); tran.Commit(); MessageBox.Show(出库成功); LoadDrugList(); } catch (Exception ex) { tran.Rollback(); MessageBox.Show(出库失败 ex.Message); } } } }这里的关键是WITH (UPDLOCK)它在读取库存时就加更新锁防止两个操作员同时出库把库存扣成负数。参数上OutReason 记录领用科室或销售去向方便后续按科室统计消耗。如果出库量大于库存直接提示并回滚不写任何流水。热搜里 c#线程 相关的并发问题在这个场景里就靠数据库锁和事务解决不需要在 C# 层自己写复杂的锁。3.3 库存预警与效期查询两个 SQL 就能搞定库存下限预警和近效期预警是药房最常用的两个查询。库存预警查 StockQty MinStock 的药品效期预警查 ExpireDate 在 90 天内且库存大于 0 的药品。-- 库存下限预警 SELECT DrugCode, DrugName, Spec, StockQty, MinStock FROM Drug WHERE IsDeleted 0 AND StockQty MinStock ORDER BY StockQty ASC; -- 近效期预警90 天内到期 SELECT DrugCode, DrugName, Spec, StockQty, ExpireDate, DATEDIFF(DAY, GETDATE(), ExpireDate) AS DaysLeft FROM Drug WHERE IsDeleted 0 AND StockQty 0 AND ExpireDate IS NOT NULL AND DATEDIFF(DAY, GETDATE(), ExpireDate) 90 ORDER BY ExpireDate ASC;DATEDIFF 算出剩余天数界面上可以把 DaysLeft 小于 30 的行标红。热搜里 sqlserver offset 后再 top 20 查到的是什么这里提醒一句如果要做分页SQL Server 2012 以上用 OFFSET FETCH但 OFFSET 是先跳过再取别和 TOP 混用搞错顺序。药品列表一般数据量不大直接全查再在 DataGridView 里排序就够不用急着上分页。4. 避坑与排查这套系统落地时最容易翻车的五个地方4.1 现象程序在自己电脑能跑拷到药房电脑就报连接失败原因连接字符串里写的是Data Source.但药房电脑上 SQL Server 装的是命名实例或者根本没装 SQL Server 只装了客户端。解决先在药房电脑上用 SSMS 连一次确认实例名和认证方式把 App.config 里的 Data Source 改成实际实例名比如.\SQLEXPRESS或192.168.1.10,1433。如果是 SQL 验证确认 sa 账号已启用且密码正确。4.2 现象入库时提示外键冲突流水插不进去原因StockIn 表有外键指向 Drug 表但传入的 DrugID 在 Drug 表里不存在常见于下拉框绑定的是显示名而不是值或者药品被软删除后仍被选中。解决检查 ComboBox 的 ValueMember 是否绑到 DrugIDDisplayMember 绑到 DrugName查询药品列表时加上WHERE IsDeleted 0避免选到已删除药品。4.3 现象两个操作员同时出库库存扣成了负数原因出库前查库存和扣库存是两步中间没有锁并发时都查到足够库存然后都扣减。解决如 3.2 所示在查询库存时加WITH (UPDLOCK)把校验和扣减放在同一个事务里让第二个操作等第一个提交后再读。4.4 现象DataGridView 绑定后修改数据不生效或者报并发冲突原因直接用 DataAdapter 的 Update 方法绑定 DataGridView但没有配 CommandBuilder 或者主键没设对。解决这个系统里我一般不用自动更新而是让用户点保存按钮走明确的 UPDATE 语句避免 DataGridView 直接改库带来的不可控。如果一定要用绑定更新确保 DataTable 有主键并且配置好 InsertCommand、UpdateCommand、DeleteCommand。4.5 现象打包成安装程序后目标机器提示缺少 .NET Framework原因安装程序没有把 .NET Framework 作为前置条件打包进去。解决在 Visual Studio 的安装项目里右键项目 → 属性 → 系统必备勾选 .NET Framework 4.5 或对应版本并选择「从与我的应用程序相同的位置下载系统必备组件」。这样生成的安装包会自带运行时药房电脑断网也能装。热搜里 winform打包成安装程序、vs2015 相关的操作核心就是这个设置。5. 从能跑到好用打包交付前我必做的三件事和一个习惯第一件事是给数据库加一个初始化脚本。交付时不可能让药房自己建表我一般会写一个InitDB.sql里面包含建库、建表、插入默认管理员账号和几条测试药品数据。安装程序第一次运行时用一个隐藏的初始化窗体执行这个脚本执行完提示「数据库初始化完成」。默认管理员密码存 SHA256 哈希不要存明文登录时把输入密码哈希后比对。第二件事是给所有查询加软删除过滤。Drug 表的 IsDeleted 字段一旦加上所有 SELECT 都要带WHERE IsDeleted 0否则删掉的药品还会出现在下拉框和列表里。这个坑我踩过删了药品结果出库时还能选到查了半天才发现是查询没过滤。第三件事是打包前把连接字符串改成可配置。不要写死在代码里用 App.config并且给安装程序加一个配置界面让实施人员填服务器地址、数据库名、账号密码。这样同一套安装包能部署到不同药房不用重新编译。最后一个习惯每次改完核心业务代码我都会手动跑一遍「入库 100 → 出库 30 → 查库存 70 → 查流水两条」这个最小闭环。别小看这个动作很多事务漏提交、参数写反的问题跑一遍就现原形。药房系统不怕功能少就怕账对不上账对了这套 WinForm SQL Server 的方案就立住了。希望帮到你。本文还有配套的精品资源点击获取
返回列表