ARTICLE DETAIL

资讯详情

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

C#药店管理系统开发实战:WinForms+SQL Server进销存设计

C#药店管理系统开发实战:WinForms+SQL Server进销存设计 简介面向计算机相关专业毕业设计或期末作业的C#药店管理系统源码包适合刚接触WinForms/WPF开发与数据库编程、需要课程设计或项目实训参考的学生。项目覆盖药品信息管理、销售记录、库存存储、权限控制等典型药房业务模块代码中体现ADO.NET/Entity Framework数据访问、MVC分层、多线程调用及用户安全设计等关键技能。压缩包共912个文件、约10.65MB其中C#源码达387个另有100个resx界面资源、85个resources资源、69个dll运行库、23个rpt报表及一批config配置文件可完整还原项目并直接编译运行。从目录看包含多个独立子项目模块边界清晰适合对照学习数据库连接、界面事件与业务逻辑的交互方式。目前已有73人浏览学习对需要快速搭建一套可演示的药店管理系统或理解C/S架构项目的读者有直接参考价值。1. 从选题到演示只有两周C#药店管理系统最忌方向选错从选题到演示往往只剩两周C#药店管理系统最怕的不是代码写不完而是方向选错导致返工。这个标题落到毕业设计和期末作业场景里通常意味着三件事一套 Windows 桌面程序、一个 SQL Server 数据库、一次能扛住评委追问的进销存演示。它要解决的是“怎么在有限时间里把药品档案、采购入库、销售开单、库存预警、员工权限这些环节串成一个闭环”的问题适合想用经典技术栈稳拿学分的学生也适合帮学生盯进度的指导教师。这篇笔记按我实际带人做这类系统的顺序展开先定选型再设计数据流落数据库写核心代码最后排掉最常见的那几个坑。2. 技术选型与项目骨架为什么 WinForms 和 SQL Server 是毕设的稳妥组合做这套 C# 药店管理系统界面框架和数据库这两件事我几乎不纠结。界面固定选 WinForms数据库固定选 SQL Server本机用随 Visual Studio 一起装好的 LocalDB 实例项目结构按 UI、BLL、DAL、Model 四层拆开。这个组合不新潮但胜在稳定网上能搜到的 C# winform 案例多学校里答辩环境大概率装得有 Visual Studio演示时最怕出现的“在我机器上能跑”这种翻车用这个组合能压到最低。2.1 界面框架WinForms 比 WPF 更适合学期末的时间表先说结论如果目标是“两周内出一版能稳定演示的系统”选 WinForms如果目标是“用界面效果当论文亮点”才考虑 WPF。WinForms 的价值在于拖控件加双击事件一个 DataGridView 就能把药品列表、销售明细这类单据页面撑起来截图放进论文里也清楚。WPF 的 XAML、数据绑定、样式模板这套东西学习周期按周算学期末的时间表等不起。这不是说 WPF 不好而是说它的优势界面表现力在毕设答辩里的性价比不够高。我用 C# 做这类系统时凡是评估过工期的最后全都落到 WinForms 上这也符合多数 C# 毕设项目的实际选型。选 WPF 的前提是你已经会写 DataTemplate 和 ListBox 绑定而不是临场现学。2.2 数据库选型LocalDB 与 Access 的取舍数据层是这套系统的核心选型失误会出现“期末演示换台机器就崩”的血泪场面。Access 最大的问题是并发写库时频繁报“记录锁定”或“数据库已被占用”多窗口同时开销售单就能复现而 SQL Server LocalDB 是随 Visual Studio 安装的零配置实例连接串指到实例名就能跑。如果学校机房装的是完整版 SQL Server连接串里换个服务器名即可平滑迁移。需要注意的是别用 AttachDbFilename 方式挂库文件路径一变库就找不到LocalDB 直接用实例名最省事。App.config 连接串我一般这样写connectionStrings add nameDrugstoreDB connectionStringServer(localdb)\MSSQLLocalDB;DatabaseDrugstore;Trusted_Connectiontrue;MultipleActiveResultSetstrue providerNameSystem.Data.SqlClient / /connectionStrings这里有两个关键参数MultipleActiveResultSetstrue允许一个连接里同时跑 DataReader 和新的 Command避免在 DataGridView 绑定数据源时报“已有打开的 DataReader”Trusted_Connectiontrue用 Windows 身份登录不用管 sa 密码。如果连不上先在 cmd 里执行sqllocaldb info看本机实例名确认是MSSQLLocalDB还是ProjectsV16这类名字再改连接串。2.3 三层架构为答辩“分层了吗”提前准备数据访问和 SQL 全写在 Form 里系统做完能跑但答辩时评委一定会问“分层了吗”“可维护性怎么体现”。所以我会在一开始就按四层结构建项目UI 层只放窗体BLL 层放业务规则比如库存不足不能销售DAL 层封装所有 SQL 操作Model 层放 Drug、User、SalesOrder 这类实体类。结构长这样Drugstore.sln ├─ Drugstore.UI // WinForms 窗体、控件事件 ├─ Drugstore.BLL // 库存判断、权限判断 ├─ Drugstore.DAL // SqlConnection / SqlCommand 封装 └─ Drugstore.Model // 实体类Drug、User、SalesOrderModel 层的存在很重要有了强类型实体后面用ListT绑定 DataGridView、用泛型封装公共方法才方便否则到处都是 DataTable代码又乱又难改。DAL 基类我习惯提供一个统一入口public static class DbHelper { public static SqlConnection OpenConnection() { var conn new SqlConnection(ConfigurationManager.ConnectionStrings[DrugstoreDB].ConnectionString); conn.Open(); return conn; } public static int ExecuteNonQuery(string sql, params SqlParameter[] parameters) { using (var conn OpenConnection()) using (var cmd new SqlCommand(sql, conn)) { if (parameters ! null) cmd.Parameters.AddRange(parameters); return cmd.ExecuteNonQuery(); } } }using保证连接用完即释放避免“数据库连接过多”这种玄学报错params SqlParameter[]强制调用方走参数化从源头堵住 SQL 注入。这套基类在登录、增删改查里全部复用答辩时也能讲清楚 DAL 层的职责边界。2.4 用泛型封装通用方法顺手拿个加分点热词里总有人问 C# 泛型案例其实这套系统里就能用。比如下拉框绑定每个窗体都要写一遍DataSource、DisplayMember、ValueMember用泛型收口一次后面所有窗体都干净public static void FillComboT(ComboBox cbo, ListT list, string displayMember, string valueMember) { cbo.DataSource list; cbo.DisplayMember displayMember; cbo.ValueMember valueMember; }调用时一行搞定FillCombo(cboSupplier, supplierList, Name, Id);。这类方法不复杂但能在代码结构答辩时展示“封装思维”比罗列功能点更能拿分。3. 功能模块与数据流设计六个模块怎么用一张销售单串起来药店管理系统说到底是进销存模块再多数据流的主线只有一条药品档案是基础入库增加库存销售扣减库存报表读流水。我通常把它拆成六个模块前端导航对应六个按钮数据库对应六组表这样论文画架构图也好画答辩讲“系统整体逻辑”也好讲。3.1 从登录到报表六个模块划分模块主界面入口核心表关键点登录与权限登录窗体UserMD5 存储密码、Role 区分管理员与收银员员工管理员工维护Employee员工档案与登录账号关联药品档案药品资料Drug条码、商品名、批准文号、生产厂家采购入库入库单StockIn、StockInDetail入库单头与明细一对多销售开单销售界面SalesOrder、SalesOrderDetail扣库存、打印小票、退药冲红库存与报表库存查询、销售报表Inventory、View_Sales低库存预警、销售趋势药品档案和库存拆成两张表是这套系统的关键Drug 存静态信息名称、规格、厂家、条码Inventory 存动态数量。入库单录入时改动 Inventory销售开单时也改动 Inventory避免直接改药品主表导致历史单据对不上账。3.2 用一条数据流串起全部模块评委最爱问的一句话是“你讲讲系统整体怎么跑”所以模块之间必须有清晰的流转关系。我的实践里这六步能自洽回答第一步在药品档案里新增一个药品第二步做采购入库单选择供应商和药品明细第三步入库完成后库存表数量增加第四步销售界面扫条码或手输药品加入购物车第五步结算时扣减库存并写入销售流水第六步报表页按时间范围统计销售金额和库存预警。每一步都有界面可点、有表可查演示时不至于只能背代码。这里要先把“库存表”定位为动态表所有模块围绕它加减切记不要让每个模块各写一套库存逻辑否则会出现“入库加的是 Drug 表、销售扣的是 Inventory 表”这种对不上的局面。3.3 扫码枪输入与条码框实现热搜词里大量出现“C# 读取条码”相关的问题实际上药店收银台用的扫码枪本质是个键盘扫完条码后自动敲了一下回车。所以前端只需要一个普通 TextBox在 KeyPress 事件里拦截回车键private void txtBarcode_KeyPress(object sender, KeyPressEventArgs e) { if (e.KeyChar (char)13) // 扫码枪默认以回车结尾 { SearchDrugByBarcode(txtBarcode.Text.Trim()); e.Handled true; // 阻止换行音 } }e.Handled true一定要写不然回车声会响还可能触发默认按钮。另外要注意把文本框的 ImeMode 设为 Disable中文输入法激活时扫码会被自动组词切断连续扫多件药品时每扫一次就查一次库存查到后列表刷新一次这是销售模块最常用的交互。条码框里我还会用一个StringBuilder做缓存防止个别扫码枪把断码分段送进来。3.4 库存预警怎么在状态栏里常驻热词里有人问“C# winform 如何更新状态栏与进度条”这里的预警就是一个标准的场景。用窗体的System.Windows.Forms.Timer每隔一段时间取一次预警数量再写进ToolStripStatusLabelprivate void timerAlert_Tick(object sender, EventArgs e) { int warningCount new DrugBll().GetWarningCount(); toolStripStatusLabelWarning.Text $库存预警 {warningCount} 种; }关键是 Timer 选型这里用窗体自带的System.Windows.Forms.Timer它的 Tick 事件跑在 UI 线程不需要跨线程 Invoke如果换成System.Timers.Timer事件在 ThreadPool 线程触发直接改状态栏文本会报“线程间操作无效”这就是很多 C# 新手卡住的点。刷新间隔我一般设 30 秒不要去扫一次库就刷新一次界面否则库存查询 SQL 会变成性能瓶颈GetWarningCount()内部只查COUNT(*)不拉明细。3.5 销售小票与 Excel 导出销售开单后打印小票和导出 Excel 是毕设的加分项但别用太重的东西。小票打印用PrintDocument按 58mm 热敏纸宽度把单据内容拼成多行文本再控制换行导出 Excel 我一般用 ClosedXML它对 .NET 项目友好不用装 Office直接生成.xlsx。这两块代码不长但要演示效果好建议做成工具类而不是写在 Form 里。4. 数据库设计与存储过程给库存扣减上一把事务锁数据库设计决定这套系统能做多深。药品信息、供应商、员工这类表按常规设计即可难的是库存扣减销售开单要同时改销售流水、扣库存、可能还要退药冲红任何一步失败都不能留下半截数据。把事务逻辑写进存储过程是毕设答辩最提档次的一个点。4.1 核心表清单这套系统里我固定用八张表User登录账号、Employee员工、Supplier供应商、Drug药品档案、Inventory库存、StockIn入库单头、StockInDetail入库明细、SalesOrder销售单头、SalesOrderDetail销售明细。其中 User 和 Employee 可以合并但分开能讲清楚“账号与员工档案”两个概念。关键的 Inventory 表只有五个字段DrugId、Quantity、WarningLevel、LastUpdated、UpdateCount动态数量全部走存储过程更新不让人在界面上直接改。4.2 药品表与库存约束的 DDLCREATE TABLE Drug ( DrugId INT IDENTITY(1,1) PRIMARY KEY, Barcode NVARCHAR(32) NOT NULL UNIQUE, DrugName NVARCHAR(100) NOT NULL, Spec NVARCHAR(50), Manufacturer NVARCHAR(100), ApprovalNo NVARCHAR(50), StopFlag BIT NOT NULL DEFAULT 0 ); CREATE TABLE Inventory ( DrugId INT PRIMARY KEY REFERENCES Drug(DrugId), Quantity INT NOT NULL DEFAULT 0, WarningLevel INT NOT NULL DEFAULT 20, LastUpdated DATETIME NOT NULL DEFAULT GETDATE(), UpdateCount INT NOT NULL DEFAULT 0 );Barcode加唯一约束防止同一个条码录两遍药品档案采用StopFlag软删除而不是直接 DELETE因为库存流水和销售单据里都引用了药品的 ID物理删除会把历史单据打断。WarningLevel默认 20 表示库存低于 20 就预警这个阈值可在界面调整它就是预警功能的数据依据。4.3 库存扣减存储过程事务与行锁销售结算时扣减库存最简单也最容易写错的做法是“先查库存够不够再 UPDATE”但两个步骤之间有时间差并发时会超卖。放到存储过程里用事务和行锁一次完成CREATE PROCEDURE usp_DeductStock DrugId INT, Quantity INT, Result INT OUTPUT -- 1 成功0 库存不足-1 异常 AS BEGIN SET NOCOUNT ON; BEGIN TRY BEGIN TRANSACTION; UPDATE Inventory WITH (ROWLOCK, UPDLOCK) SET Quantity Quantity - Quantity, LastUpdated GETDATE(), UpdateCount UpdateCount 1 WHERE DrugId DrugId; IF ROWCOUNT 0 BEGIN SET Result -1; ROLLBACK; RETURN; END IF NOT EXISTS (SELECT 1 FROM Inventory WHERE DrugId DrugId AND Quantity 0) BEGIN SET Result 0; ROLLBACK; RETURN; END -- 同时可以把销售明细写入这里保证同一个事务 -- INSERT INTO SalesOrderDetail ... COMMIT; SET Result 1; END TRY BEGIN CATCH IF TRANCOUNT 0 ROLLBACK; SET Result -1; END CATCH END这里有两个关键细节。WITH (ROWLOCK, UPDLOCK)是行级更新锁防止两个收银台同时卖同一盒药UPDATE 执行期间另一会话必须等待从根上避免超卖IF NOT EXISTS ... WHERE Quantity 0在扣减后立刻校验负库存库存不足就回滚不会留下负数。调用时用参数接收返回值业务层根据Result决定提示“库存不足”还是继续走结算流程。4.4 软删除与追溯别给自己埋后悔药药品、供应商这类基础档案被销售单引用时一旦硬删除外键约束会产生级联冲突或导致单据显示成空行。我的习惯是所有基础表都加StopFlag下架药品时置 1查询默认带WHERE StopFlag 0销售历史里则仍能看到原名称。这看起来多写几行代码但能避免演示当天“删了个药品报表统计全乱”这种事故。5. 避坑排查五个必踩的坑从 DataGridView 卡死到金额精度这套系统里最容易让人熬夜的往往不是业务复杂而是 C# 本身那几个经典坑。以下五条我几乎每次带人做都会遇到写成现象、原因、解决三段能帮你少走一半弯路。5.1 坑一DataGridView 绑定后界面卡死现象后台线程里改了数据源界面直接变成“未响应”十几秒后弹“线程间操作无效从未创建该控件的线程访问它”。原因DataGridView 的DataSource必须在 UI 线程赋值后台线程直接操作控件属于跨线程访问。解决在 UI 线程里用Invoke包装刷新动作private void RefreshDrugGrid(ListDrug drugs) { if (this.dataGridView1.InvokeRequired) { this.Invoke(new ActionListDrug(RefreshDrugGrid), drugs); return; } drugBindingSource.DataSource drugs; drugBindingSource.ResetBindings(false); }InvokeRequired判断当前线程是后台线程就切回 UI 线程再执行ResetBindings(false)通知 DataGridView 重新拉一遍数据。绑定数据源建议用BindingSource而不是直接给DataSource赋ListT后续增删行时刷新更可控。这个坑在热词里高频出现属于 C# winform 绕不过去的一道坎。5.2 坑二金额计算用 double对账差一分现象某几单售价乘数量再加折扣算出的总价跟手工算差 0.01 元。原因double是二进制浮点0.1 这类小数无法精确表示累加次数多了误差就出来。解决所有金额一律用decimal类型。decimal在 C# 里就是为货币设计的十进制浮点加减乘除都按十进制精度算对账永远不会差一分decimal price 12.5m; // 单价用 m 后缀 int quantity 3; decimal total price * quantity; // 结果精确为 37.50注意 SQL Server 侧字段要用DECIMAL(18,2)不要用FLOAT从数据库读出金额时Convert.ToDecimal(reader[TotalAmount])不要用Convert.ToDouble。5.3 坑三登录用字符串拼接 SQL一道注入送走答辩现象登录框输入 OR 11系统直接以管理员身份进去了。原因SQL 语句用字符串拼接把用户输入当成代码执行。解决一律参数化这也是我在 DAL 层DbHelper里强制SqlParameter的原因string sql SELECT COUNT(1) FROM [User] WHERE LoginName name AND PwdHash pwd; var parameters new[] { new SqlParameter(name, txtLogin.Text.Trim()), new SqlParameter(pwd, MD5Hash(txtPwd.Text)) }; int count (int)DbHelper.ExecuteScalar(sql, parameters);参数化以后用户输入只会被当作“值”永远不可能变成 SQL 语法。密码也不要存明文至少做一层 MD5答辩时被问到安全也不心虚。5.4 坑四Timer 跨线程访问控件状态栏不刷新现象主窗口用了System.Timers.Timer定时刷新库存预警运行偶尔闪退调试时提示“线程间操作无效”。原因System.Timers.Timer的Elapsed事件跑在 ThreadPool 线程直接改 UI 控件属于非法跨线程。解决窗体刷新这种 UI 轮询直接改用System.Windows.Forms.Timerthis.timerAlert.Interval 30000; // 30 秒 this.timerAlert.Tick timerAlert_Tick; this.timerAlert.Start();它的 Tick 在 UI 线程触发没有跨线程问题。只有做后台定时任务比如自动备份才需要后台线程那时改 UI 仍然要走Invoke。这个选择不是玄学而是两种 Timer 的线程模型差异决定的。5.5 坑五Access 数据库被占用演示当场翻车现象用 Access 做完上半场功能期末演示换到机房电脑一运行就报“文件正在被另一进程使用”或“无法更新数据库或对象为只读”。原因Access 是文件型数据库对多用户并发写锁很脆弱程序异常退出时.ldb锁文件残留也会导致占用。解决数据库选 SQL Server LocalDB把连接串换过来。如果时间来不及换至少保证每次操作都用using及时Dispose连接并且不要长时间持有OleDbConnectionusing (var conn new SqlConnection(connstr)) { conn.Open(); // 执行操作用完即关 }用using包住连接作用域结束就释放避免连接占着不放。长痛不如短痛药店管理系统这种多窗口并发的场景Access 确实不适合别在选型上省几天时间后面付出的是几周排错成本。6. 验收与答辩技巧三分钟的演示顺序和评委最爱问的三个问题系统做完先别急着交按下面这个顺序自己走一遍能覆盖绝大多数功能点。第一步登录演示管理员与收银员权限差异第二步在药品档案里新增一个药品体现录入和校验第三步做一张采购入库单库存数量随之变化第四步回到销售界面用条码框扫一件刚入库的药品完成结算说明“库存同步扣减”第五步把库存数量调到预警线以下展示状态栏预警第六步打开销售报表按今天统计出单数和销售额。整个过程三分钟每个动作都对应一个模块评委跟着你的顺序看下来自然能理解“进销存闭环”这个核心设计。评委最爱问的三个问题提前想好答案。“你怎么保证库存不超卖”回答存储过程里的事务和行锁举usp_DeductStock为例。“系统分层体现在哪”回答 UI、BLL、DAL、Model 四层数据访问集中在 DAL。“如果从 SQL Server 换成 MySQL 要改多少”回答只改 DAL 层实现界面和业务层不动因为上层依赖的是方法而不是 SQL。这三个问题答得顺畅比功能完整更能拉分。我当年第一个版本把扣库存逻辑散写在各个 Form 里结果越改越乱最后查出四处在改同一张库存表对账对不上。后来带别人做这类系统第一件事就是先把存储过程和 DAL 层固定下来再谈界面。这也成了我做 C# 项目的习惯先锁数据链路再长界面枝叶。希望帮到你。本文还有配套的精品资源点击获取
返回列表