
简介一套基于C#开发的药店管理系统完整项目适用于计算机相关专业学生的毕业设计或期末课程作业也可作为Windows桌面应用开发的练手素材。压缩包共912个文件大小约10.65MB包含387个cs源码文件、100个resx界面布局文件、85个resources资源文件、69个dll动态库、23个rpt报表文件以及exe可执行程序等基本覆盖UI界面、业务逻辑、数据持久化与报表输出各个环节。目前已有73人学习浏览适合需要快速搭建课设框架的开发者参考。项目涉及数据库管理基于ADO.NET/Entity Framework、Windows Forms/WPF界面开发、MVC分层架构、用户权限与安全控制等关键知识点目录结构较为清晰便于按模块阅读和二次扩展由于资源来自网络分享使用时建议结合自身环境进行调试与适配。1. 从“能交差”到“能答辩”这套C#药店管理系统到底在做什么说起C#药店管理系统网上一搜一大堆但绝大多数是同一个模子倒出来的一个登录界面、几个增删改查表单、数据能存进去再读出来界面上摆个DataGridView就完事。这套东西作为C#课程设计能糊弄过去但拿到毕业设计答辩桌上老师随便问一句“库存不足怎么预警”“销售时并发怎么写”“药品过期批次怎么处理”十有八九就卡壳了。这篇文章讲的这套基于C#的药店管理系统核心不是把界面做得有多花哨而是把“进销存”这条业务链走完整药品入库、分类管理、库存预警、前台收银、销售明细、供应商往来最后再配上用户权限和基础报表。整套系统的代码量不大胜在结构清晰、表关系干净、踩坑点集中尤其适合用来做毕业设计或期末作业——你不用为了“显得高级”硬塞一堆用不上的设计模式把核心业务写明白老师问什么你都能接得住。接下来的内容我会按一条主线往下走先交代技术栈和开发环境的取舍然后把数据库表结构拆开看接着是三层架构怎么搭、核心业务怎么写最后集中讲我会在毕设答辩前特意检查的几类坑。你跟着把思路理顺代码只是顺带的事。2. 技术栈选型为什么用WinForms而不是WPF以及环境怎么一次性配好2.1 选型逻辑面向毕业设计的C#技术栈取舍做药店管理系统这类偏传统的信息管理系统最常见的C#方案有三种ASP.NET WebForm/WPF/WinForms。对于期末作业和本科毕业设计我强烈建议用WinForms SQL Server或LocalDB这套组合原因如下上手门槛低WinForms的拖拽式界面设计对新手极其友好代码结构直观不会像WPF那样上来就要理解DataTemplate、Binding、依赖属性这些概念。演示方便单机运行不需要部署IIS或配置Web环境答辩时打开Visual Studio点一下F5就能跑出问题的概率最小。源码可读性强相比Web项目WinForms的事件驱动模型容易讲清楚——按钮点击触发什么、查询怎么执行、数据怎么绑定每一条都能在答辩时展开说。这里有个常见的误解用WPF会更“高级”能给答辩加分。但如果你的核心业务还没有完全跑通WPF的学习成本会直接把进度拖垮。毕设答辩老师更看重的是业务逻辑是否完整、数据设计是否合理而不是用了什么UI框架。2.2 环境搭建的完整清单与验证方式在你动手写第一行代码之前先把环境配齐。以下是推荐的组合和验证方法组件推荐版本说明开发工具Visual Studio 2022 Community免费自带的WinForms模板.NET版本.NET Framework 4.7.2 或 .NET 6/8建议选Framework 4.7.2兼容性最稳数据库SQL Server 2019/2022 Express免费版够用或者用LocalDB数据库管理SSMSSQL Server Management Studio建表、写脚本用验证环境是否就绪的方式我一般会这样做新建一个WinForms项目放一个Button和一个DataGridView连上本地数据库跑一个最简单的SELECT能显示数据就说明整套链路通了。确认Visual Studio的“工具 → 连接到数据库”能正常连到你的数据库实例这样可以省去之后反复改连接字符串的麻烦。3. 数据库设计先行药店管理系统的核心表结构与关系建模3.1 数据表拆解从药品主数据到销售流水做管理系统第一步永远是设计表。表结构设计得好后面写代码就像填空设计得不好写到一半你会发现售后查询查不出来、报表统计对不上数只能回头改表——那是最伤进度的事。这套系统我建议至少拆成以下6张表表名用途关键字段用户表Users登录与权限控制用户ID、用户名、密码、角色药品表Medicines药品基础信息药品ID、名称、规格、生产厂家、批准文号分类表Categories药品分类分类ID、名称、备注库存表Stocks各批次的库存数量库存ID、药品ID、批次号、数量、过期日期销售表Sales销售订单头销售ID、用户ID、销售时间、总金额销售明细表SaleItems商品级销售记录明细ID、销售ID、药品ID、数量、单价有些同学会问为什么不直接把库存字段放进药品表一开始我也这么干过但很快发现问题——药品有批次关联同一个药品可以有两个批次附近期先出FEFO否则批号混乱就会导致过期药品先卖出去。库存必须按批次维度拆开这是药店系统的第一道坎。3.2 建表脚本一次写对主键、外键和索引这是我常用的建表脚本精简版适配SQL ServerCREATE TABLE Categories ( CategoryId INT PRIMARY KEY IDENTITY(1,1), CategoryName NVARCHAR(50) NOT NULL, Remark NVARCHAR(200) NULL ); CREATE TABLE Medicines ( MedicineId INT PRIMARY KEY IDENTITY(1,1), MedicineCode NVARCHAR(30) NOT NULL UNIQUE, MedicineName NVARCHAR(100) NOT NULL, Spec NVARCHAR(50) NULL, Manufacturer NVARCHAR(100) NULL, CategoryId INT FOREIGN KEY REFERENCES Categories(CategoryId), ApprovalNumber NVARCHAR(50) NULL ); CREATE TABLE Stocks ( StockId INT PRIMARY KEY IDENTITY(1,1), MedicineId INT FOREIGN KEY REFERENCES Medicines(MedicineId), BatchNumber NVARCHAR(50) NOT NULL, Quantity INT NOT NULL DEFAULT 0, ExpiryDate DATE NOT NULL ); CREATE TABLE Sales ( SaleId INT PRIMARY KEY IDENTITY(1,1), UserId INT, SaleTime DATETIME NOT NULL DEFAULT GETDATE(), TotalAmount DECIMAL(10,2) NOT NULL DEFAULT 0, FOREIGN KEY (UserId) REFERENCES Users(UserId) ); CREATE TABLE SaleItems ( SaleItemId INT PRIMARY KEY IDENTITY(1,1), SaleId INT FOREIGN KEY REFERENCES Sales(SaleId), MedicineId INT FOREIGN KEY REFERENCES Medicines(MedicineId), Quantity INT NOT NULL, UnitPrice DECIMAL(10,2) NOT NULL, Amount DECIMAL(10,2) NOT NULL );几点说明Medicines表的MedicineCode字段建议加UNIQUE约束这是药监码或自编码价格加唯一性约束之后才能在销售按编码查药品时避免重名混淆。Stocks表一定要建ExpiryDate字段这是后面做过期预警的数据底座。DECIMAL(10,2)用于金额不要用FLOAT或REAL否则对账时会出现0.1 0.2不等于0.3的尴尬局面。Sales和SaleItems是一对多的关系销售表记录“谁在什么时间买了什么”明细表记录“这次销售里具体包含了哪些药品”两张表的关系会影响后面写销售逻辑时的整体思路。3.3 关系梳理用外键把业务串起来表关系不复杂但你在答辩时要能讲清楚为什么这么分药品表属于分类表一个分类下有多条药品所以Medicines.CategoryId外键指向Categories.CategoryId。库存表属于药品表一种药品按批次拆成多条库存记录所以Stocks.MedicineId外键指向Medicines.MedicineId。销售明细表属于销售表一张销售单可能买三种药所以SaleItems.SaleId外键指向Sales.SaleId。这套模型把“库存”和“销售”两个核心模块解耦了。销售发生时只要去查Stocks表里的可用批次并扣减对应数量入库时只需要往Stocks表插一条批次记录。逻辑清晰代码也好写。4. 三层架构落地UI层、业务层、数据访问层怎么分工4.1 为什么用三层架构而不是把所有代码塞进窗体很多期末作业的写法是数据库连接、SQL语句、逻辑判断全部写在按钮的Click事件里。这种写法能跑但有个致命问题——一旦某个查询需要复用比如登录后查询库存、销售时也要查询库存你就得复制粘贴一遍代码。如果哪天改了表结构需要把所有写SQL的地方都翻出来改一遍改漏了就直接翻车。用三层架构的意义不是装模作样贴金而是把“界面”和“逻辑”分开UI层Views只负责显示和收集输入不做任何SQL操作。业务层BLL处理规则例如销售时校验库存够不够、入库时计算是否应该预警。数据访问层DAL封装所有SQL操作上层只传参数进来拿到结果出去。写三层架构的代码时有一个口诀界面尽量薄业务尽量厚数据访问尽量瘦。4.2 从Connection到Command最简洁的数据访问层写法数据访问层我习惯用一个工具类封装避免每个窗体里都写一遍连接字符串using System; using System.Data; using System.Data.SqlClient; namespace PharmacyManagement.DAL { public static class SqlHelper { // 注意连接字符串不要硬编码在这里建议放到app.config中统一管理 private static readonly string connectionString System.Configuration.ConfigurationManager.ConnectionStrings[PharmacyDB].ConnectionString; public static DataTable ExecuteDataTable(string sql, params SqlParameter[] parameters) { using SqlConnection conn new SqlConnection(connectionString); using SqlCommand cmd new SqlCommand(sql, conn); if (parameters ! null) cmd.Parameters.AddRange(parameters); SqlDataAdapter adapter new SqlDataAdapter(cmd); DataTable dt new DataTable(); adapter.Fill(dt); return dt; } public static int ExecuteNonQuery(string sql, params SqlParameter[] parameters) { using SqlConnection conn new SqlConnection(connectionString); using SqlCommand cmd new SqlCommand(sql, conn); if (parameters ! null) cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteNonQuery(); } } }这段代码里需要重点理解params SqlParameter[] parameters以参数化方式传值永远不要在SQL语句中拼接用户输入否则 OR 11这种注入就是几秒钟的事。using关键字确保数据库连接用完后自动关闭。不写Close()在这个写法下也不会有连接泄露问题因为Dispose()会接管收尾工作。DataTable作为返回类型因为一整个窗体的DataGridView.DataSource可以直接绑定DataTable这是最省事的模式不需要额外写列表转换。4.3 业务层的两个核心方向销售流程与库存扣减业务层是这套系统最值得花时间的部分。以“前台销售”为例完成一次销售不是往Sales表插一条记录那么简单而是要完成三个动作插入销售主表记录获得SaleId逐条插入销售明细表记录扣减对应批次的库存数量下面这是精简版销售业务代码using System; using System.Data; using System.Data.SqlClient; using PharmacyManagement.DAL; namespace PharmacyManagement.BLL { public class SaleService { public bool CreateSale(DataTable saleItems, int userId) { // 逐项检查库存是否充足 foreach (DataRow row in saleItems.Rows) { int medicineId Convert.ToInt32(row[MedicineId]); int quantity Convert.ToInt32(row[Quantity]); string sqlCheck SELECT SUM(Quantity) AS TotalStock FROM Stocks WHERE MedicineId mid; SqlParameter[] checkParams { new SqlParameter(mid, medicineId) }; DataTable stockDt SqlHelper.ExecuteDataTable(sqlCheck, checkParams); int totalStock stockDt.Rows[0][TotalStock] DBNull.Value ? 0 : Convert.ToInt32(stockDt.Rows[0][TotalStock]); if (totalStock quantity) { return false; // 库存不足终止整个销售 } } // 通过事务保证主表和明细表要么全部成功要么全部回滚 using SqlConnection conn new SqlConnection(connectionString); conn.Open(); using SqlTransaction transaction conn.BeginTransaction(); try { // 插入销售主表拿到自增ID string sqlSale INSERT INTO Sales(UserId, SaleTime, TotalAmount) OUTPUT INSERTED.SaleId VALUES(uid, GETDATE(), 0); using SqlCommand cmdSale new SqlCommand(sqlSale, conn, transaction); cmdSale.Parameters.AddWithValue(uid, userId); int saleId (int)cmdSale.ExecuteScalar(); // 逐条插入明细并扣减库存 foreach (DataRow row in saleItems.Rows) { int medicineId Convert.ToInt32(row[MedicineId]); int quantity Convert.ToInt32(row[Quantity]); decimal unitPrice Convert.ToDecimal(row[UnitPrice]); string sqlDetail INSERT INTO SaleItems(SaleId, MedicineId, Quantity, UnitPrice, Amount) VALUES(sid, mid, qty, price, amount); using SqlCommand cmdDetail new SqlCommand(sqlDetail, conn, transaction); cmdDetail.Parameters.AddWithValue(sid, saleId); cmdDetail.Parameters.AddWithValue(mid, medicineId); cmdDetail.Parameters.AddWithValue(qty, quantity); cmdDetail.Parameters.AddWithValue(price, unitPrice); cmdDetail.Parameters.AddWithValue(amount, unitPrice * quantity); cmdDetail.ExecuteNonQuery(); // 优先扣减最早过期的批次FEFO策略 string sqlStock UPDATE Stocks SET Quantity Quantity - qty WHERE StockId ( SELECT TOP 1 StockId FROM Stocks WHERE MedicineId mid AND Quantity 0 ORDER BY ExpiryDate ASC ); using SqlCommand cmdStock new SqlCommand(sqlStock, conn, transaction); cmdStock.Parameters.AddWithValue(mid, medicineId); cmdStock.Parameters.AddValueWith(qty, quantity); cmdStock.ExecuteNonQuery(); } transaction.Commit(); return true; } catch { transaction.Rollback(); return false; } } } }几个容易出错的地方库存检查用的SUM(Quantity)是“总库存”但真实业务中药品分成多个过期批次总库存表里的数量够不代表某个批次能用。如果追求更严谨销售界面应该让操作员选择批次这就涉及UI层面的设计。扣减库存的SQL用TOP 1配合ORDER BY ExpiryDate ASC这是过期日期最早优先FEFO药店管理的核心场景——先批次的药必须先卖出去否则临期药滞销报损时直接亏损。整个操作必须包在事务里如果只成功插了销售明细库存扣减时报错那账面就全乱了。这也是答辩时老师最爱问的点务必能讲清楚。4.4 把连接字符串挪到配置文件里有些同学的代码里把连接字符串硬编码在SqlConnection里。这有个风险换电脑后数据库实例名不一样你得重新编译一遍才能运行。正确的做法是把连接字符串放在App.config中connectionStrings add namePharmacyDB connectionStringData Source.;Initial CatalogPharmacyDB;Integrated SecurityTrue providerNameSystem.Data.SqlClient / /connectionStrings改配置只需改Data Source这里。Data Source.表示本机默认实例如果装的是Express版要写成.\\SQLEXPRESS使用LocalDB则更像(localdb)\\MSSQLLocalDB——把这一行调整好换电脑部署时省去大量编译调试的时间。5. 避坑指南毕设答辩前最容易卡住的四个真实问题5.1 登录界面“点击登录无反应”现象界面没报错也没有跳转按了按钮像没按一样。原因你的事件没有正确绑定。WinForms在两种情况下会出现这个问题一个是拖控件时自动生成的事件名被改动了造成InitializeComponent()里的委托指向了一个不存在的方法另一个是在Load事件里做了一些初始化逻辑异常被吞掉。解决按F5跑调试模式看“输出”窗口有没有异常信息同时检查Form1.Designer.cs里按钮的Click 是否和后台方法名一致。我习惯在设计界面双击按钮生成事件而不是手动改代码从根源上避免事件绑错。5.2 数据库连接报错“无法打开登录所请求的数据库”现象程序一跑起来任何查询都报错。原因要么连接字符串写错数据库名不对或服务器实例不对要么SQL Server未启用混合登录模式。解决第一确认连接字符串里Initial Catalog是你实际建库的名称第二在SSMS里用同一个登录名和密码测试能否连上。另外SQL Server的“服务器属性→安全性”里如果选了“仅Windows身份验证”那就用Integrated SecurityTrue连如果要用用户名密码登录需要开启“SQL Server和Windows身份验证模式”并把sa账户启用、重置密码。5.3 程序正常启动但DataGridView不显示数据现象界面正常DataGridView是空的不报错。原因大概率是查询返回了空表或者DataSource没有正确赋值。还有一个常见情况你使用了DataTable作为数据源但没调用BindingSource或者表单控件被某种错误操作覆盖了。解决在绑定前用MessageBox.Show(dt.Rows.Count.ToString())确认拿回的行数。行数为0说明SQL有语法或业务层面的问题行数大于0却没显示则检查DataGridView是否设置了AutoGenerateColumns true或者手动配置的列名与返回字段不对应。5.4 日期时间格式在写库时变成“字符串转换失败”现象保存有效期或销售时间报错说无法将字符串转换为日期。原因常见于直接从DateTimePicker控件的值转成字符串再拼入SQL。不同区域设置下DateTime的字符串格式可能是yyyy/MM/dd也可能是dd-MM-yyyy数据库不认这个格式就报错。解决永远不要把日期作为字符串传给SQL直接用参数化cmd.Parameters.AddWithValue(expiryDate, dateTimePicker1.Value.Date);DateTimePicker.Value本身就是一个DateTime对象直接传对象给SqlParameterSQL Server能正确处理完全跳过格式化问题。6. 加分项设计用“事务权限”把系统从作业提升到毕设水准6.1 为什么说“销售后用事务”和“按角色控制按钮”很加分答辩时的评价标准通常不是“能不能跑”而是“你怎么解释你的设计”。随便找一个网上的作业代码老师问一句“如果两个员工同时销售同一种药系统会发生什么”很多同学答不上来因为代码里根本没有并发控制。这里给两个可以写进论文和展示的设计代码量不大但能明显提升说服力。第一个是事务控制销售时写主表、写明细、扣库存三步必须用SqlTransaction包起来。这个我在4.3节的代码里已经演示过你把那段逻辑用自己的话能讲清楚比如“如果明细写得一半库存没了整个单子回滚不产生脏数据”这就是一次合格的答辩回答。第二个是权限模型角色字段不是写着玩的。你在Users表加一个Role字段如“管理员”和“收银员”登录后把角色存到全局静态变量里public static class CurrentUser { public static int UserId { get; set; } public static string UserName { get; set; } public static string Role { get; set; } }在窗体的Load事件里做按钮级控制if (CurrentUser.Role ! Admin) { btnAddMedicine.Visible false; btnDeleteMedicine.Visible false; }这样整个系统的逻辑闭环就出来了管理员可以维护药品主数据和用户收银员只能在前台销售页面操作。演示时切换到不同的账号登录界面上能显示的功能实时变化比PPT上讲半天都有说服力。6.2 主动准备好三个会被追问的细节答辩的时候老师经常会围绕一个很小的点往下钻。下面这三个细节建议你提前准备第一个细节库存为负怎么办。如果你的扣减SQL在库存数量为0时仍然执行数据库里会出现负数库存。最简单的防线是在Stocks表加一个约束检查确保不会扣成负数ALTER TABLE Stocks ADD CONSTRAINT CK_Stock_NonNegative CHECK (Quantity 0);我再重复强调一下设了这个约束之后试图扣超过剩余库存的数量时SQL Server会直接抛异常业务层捕获后返回“库存不足”某种程度上这是最后一道防线经常能兜住你不小心留下的业务逻辑漏洞。第二个细节药品大全怎么搜。只支持按全名搜索是不够的。实际场景里操作员可能会输入“阿莫西林”的一部分来找药。推荐你使用LIKE配合通配符SELECT * FROM Medicines WHERE MedicineName LIKE keyword % OR MedicineCode LIKE keyword %参数值传%加关键字实现模糊搜索这块代码短但很实用。第三个细节过期预警怎么展示。你要能直接在界面上列出ExpiryDate在90天内的库存批次string sql SELECT m.MedicineName, s.BatchNumber, s.ExpiryDate, s.Quantity FROM Stocks s INNER JOIN Medicines m ON s.MedicineId m.MedicineId WHERE s.ExpiryDate BETWEEN GETDATE() AND DATEADD(DAY, 90, GETDATE());这段SQL跑出来再把结果绑定到独立窗口的DataGridView就是最直观的预警列表。答辩时演示一下老师很快就理解你已经把业务流程打通了不是在堆CRUD代码。最后说一个我自己的习惯不要等整个项目写完才开始写论文从建表那天起就顺手截图、记录设计理由。答辩时老师翻到你的数据库关系图和数据字典印象分会好很多。这套系统从头到尾梳理一遍你会发现业务逻辑本身并不复杂但每一步都想清楚为什么这么做你的毕业设计和期末作业就真正有了完整度——写代码时顺手能去补的细节不要拖拖到最后赶工很多就是补不完的坑。希望这些路线和避坑笔记能帮到你。本文还有配套的精品资源点击获取