简介:基于C#的医药销售管理系统完整项目,面向需要掌握C# WinForm开发与SQL Server数据库操作的计算机专业学生或初级开发者。系统涵盖药品信息管理、查询等核心模块,可直接附加数据库并登录使用,便于学习典型进销存业务流程。压缩包共290个文件,约2.49MB,以78个cs源码文件、73个ico图标、72个resx资源文件为主,另有数据库文件(mdf、ldf)、SQL脚本、项目工程文件等,代码结构完整清晰。已有141人学习下载。通过该项目可同时了解C#窗体界面设计、事件处理、数据绑定以及数据库表设计、存储过程调用等实践技巧;数据库文件直接附加SQL Server 2000即可运行,管理员账号密码均为8,方便快速验证功能。适合课程设计、毕业设计或自学参考。
1. 医药销售管理系统,C#源码加数据库的 zip 里到底装着什么
接到小药店的进销存需求时,你会发现它和超市进销存完全是两码事——同一个药品有两个批号,就是两批货,进货价、有效期、库存都得分开算,卖的时候还得优先卖快过期的。曾经就有门店因为漏了效期管理,一批感冒药压在货架上过期,几千块直接打了水漂。这套基于C#的医药销售管理系统,就是围绕「批号 + 效期」这个核心来设计的:C# WinForms 做界面,三层架构组织源码,搭配 SQL Server 数据库脚本和一整套针对医药场景的表结构。
这套项目适合三类人:正在做数据库课程设计、需要完整骨架参考的学生;想给中小药店、诊所做内部信息化改造的开发人员;以及 C# 入门之后想看看「增删改查之外,真实业务系统还有什么」的从业者。你拿到的不只是能跑的代码,更是一份把药品、批次、效期、销售、库存这些实体串起来的完整数据模型。本篇按我从数据模型到部署上线的顺序,把每一层的关键设计和踩过的坑都展开讲。
2. 先管好批号和效期:医药销售系统的数据库设计
药品管理系统的第一张表不是「药品表」,而是「批次表」。药品本身只有通用名、规格、厂家这些静态属性,真正参与库存和资金流转的是「某个批号的某批货」。建模时把这层拆开,后面的效期预警、先进先出、批次追溯才写得动。
2.1 一药多批次:为什么药品表和批次表必须分开
很多新手会把「批号」和「有效期」直接做成药品表里的两个字段,这是最典型的翻车设计。同一盒药下一次进货批号就变了,如果批号写在药品表里,意味着每进一次货就要新建一个药品档案,同一个通用名会出现好几条记录,销售开单时根本不知道该选哪条。
正确的做法是拆成两张表:药品表存「这药是什么」,批次表存「这批货从哪来、什么时候到期、还剩多少」。
2.2 建库脚本:药品、批次、销售、采购全套表结构
以下脚本在 SQL Server 2012 及以上版本可直接执行,整合了系统所需的全部基础表与主外键关系。
-- 建立药品主表:静态属性都在这里 CREATE TABLE Medicine ( MedicineId INT IDENTITY(1,1) PRIMARY KEY, MedicineCode NVARCHAR(30) NOT NULL, -- 药品编码,店内自编或按国药准字 CommonName NVARCHAR(50) NOT NULL, -- 通用名,如 阿莫西林胶囊 TradeName NVARCHAR(50) NULL, -- 商品名,如 阿莫仙 Spec NVARCHAR(50) NULL, -- 规格,如 0.25g*24粒 Manufacturer NVARCHAR(80) NULL, -- 生产厂家 ApprovalNo NVARCHAR(50) NULL, -- 批准文号 PrescriptionType INT DEFAULT 1, -- 处方类型:0 处方药 1 非处方药 Unit NVARCHAR(10) DEFAULT '盒', -- 销售单位 PurchasePrice DECIMAL(10,2) NOT NULL, -- 最近采购价,仅参考 SalePrice DECIMAL(10,2) NOT NULL, -- 零售价 MemberPrice DECIMAL(10,2) NULL, -- 会员价,允许为空 StockWarning INT DEFAULT 10, -- 库存预警阈值 Status INT DEFAULT 1 -- 1 在售 0 停售 ); -- 批次表:一次采购一个批次一条记录 CREATE TABLE MedicineBatch ( BatchId INT IDENTITY(1,1) PRIMARY KEY, MedicineId INT NOT NULL REFERENCES Medicine(MedicineId), BatchNo NVARCHAR(50) NOT NULL, -- 生产批号,同一药品不同批次不能重复 ProduceDate DATE NULL, -- 生产日期 ExpireDate DATE NOT NULL, -- 有效期至,效期预警靠它 Stock INT NOT NULL DEFAULT 0, -- 该批次剩余库存 PurchasePrice DECIMAL(10,2) NOT NULL -- 该批次实际进价,成本核算用 ); -- 往来单位表:合并客户和供应商为一张表,用 Type 区分 CREATE TABLE Partner ( PartnerId INT IDENTITY(1,1) PRIMARY KEY, PartnerName NVARCHAR(50) NOT NULL, Contact NVARCHAR(20) NULL, Phone NVARCHAR(20) NULL, PartnerType INT DEFAULT 0, -- 0 客户 1 供应商 Status INT DEFAULT 1 ); -- 销售主表:一张单子的头部信息 CREATE TABLE SaleOrder ( OrderId INT IDENTITY(1,1) PRIMARY KEY, OrderNo NVARCHAR(20) NOT NULL, CustomerId INT NULL REFERENCES Partner(PartnerId), SaleDate DATETIME DEFAULT GETDATE(), StaffName NVARCHAR(20) NULL, -- 营业员/开单员 TotalAmount DECIMAL(10,2) DEFAULT 0, -- 应收总额 DiscAmount DECIMAL(10,2) DEFAULT 0, -- 折扣金额 PayAmount DECIMAL(10,2) DEFAULT 0 -- 实收金额 ); -- 销售明细表:核心冗余策略在后面的 Note 里说明 CREATE TABLE SaleOrderDetail ( DetailId INT IDENTITY(1,1) PRIMARY KEY, OrderId INT NOT NULL REFERENCES SaleOrder(OrderId), MedicineId INT NOT NULL, MedicineName NVARCHAR(50) NOT NULL, -- 冗余快照 BatchNo NVARCHAR(50) NOT NULL, -- 冗余批次号 Spec NVARCHAR(50) NULL, -- 冗余规格 Quantity INT NOT NULL, Price DECIMAL(10,2) NOT NULL, -- 成交单价 Amount DECIMAL(10,2) NOT NULL -- 行合计 ); -- 采购主表 + 采购明细表,字段逻辑同销售 CREATE TABLE PurchaseOrder ( OrderId INT IDENTITY(1,1) PRIMARY KEY, OrderNo NVARCHAR(20) NOT NULL, SupId INT NULL REFERENCES Partner(PartnerId), OrderDate DATETIME DEFAULT GETDATE(), TotalAmount DECIMAL(10,2) DEFAULT 0 ); CREATE TABLE PurchaseOrderDetail ( DetailId INT IDENTITY(1,1) PRIMARY KEY, OrderId INT NOT NULL REFERENCES PurchaseOrder(OrderId), MedicineId INT NOT NULL, BatchNo NVARCHAR(50) NOT NULL, Quantity INT NOT NULL, Price DECIMAL(10,2) NOT NULL, Amount DECIMAL(10,2) NOT NULL, ExpireDate DATE NOT NULL -- 采购入库即登记效期 ); -- 操作员表:系统登录用 CREATE TABLE SysUser ( UserId INT IDENTITY(1,1) PRIMARY KEY, UserName NVARCHAR(20) NOT NULL, PassWord NVARCHAR(50) NOT NULL, -- 演示项目可直接存 MD5 值,生产系统务必加盐 RoleName NVARCHAR(20) DEFAULT '店员' );这段脚本做完后,整个系统的骨架已经有了:两个核心主表(Medicine、MedicineBatch)加上销售采购两组单据表。所有金额字段都用 DECIMAL(10,2) 而不是 FLOAT,医药对账一分钱都不能差,浮点误差会让你对不上账——这是用钱买来的教训。日期字段里,ExpireDate 是唯一决定效期预警的字段,入库时必填,细节见下。
2.3 明细表里的快照字段:为什么销售单不能直接联表查药名
SaleOrderDetail 里有 MedicineName、BatchNo、Spec 三个看起来「多余」的字段——明明 JOIN 一下 Medicine 表就能拿到的名字,为什么要存一份?因为药品信息是会变的:今天这药叫「阿莫西林胶囊」,明天厂家改包装改名,如果销售单只存 MedicineId,历史单据上的药名就会跟着变成新名字。
快照字段的本质是「单据一旦落库,就复刻当次交易发生时的现场」。以后查三个月前的销售记录,明细表里显示的规格、药名、单价就是成交那一刻的值,不会因后来改档案而错乱。这也是审计和追溯的基础。
3. 源码的骨架:三层架构与连接字符串这样搭
代码组织上,我用的是最常见的三层架构:UI(窗体)、BLL(业务逻辑)、DAL(数据访问),外加 Model(实体)。药品销售系统业务不算复杂,但把 SQL 直接写在按钮点击事件里,前期看起来很快,后期效期预警、批次扣减这类逻辑会到处重复,改一处漏三处。
3.1 项目结构:谁该放在哪一层
拿到源码后先在 Visual Studio 里看解决方案结构,通常长这样:
MedicineSale.sln ├── MedicineSale.UI // WinForms 层,放窗体 │ ├── Forms │ │ ├── FrmLogin.cs │ │ ├── FrmMain.cs │ │ ├── FrmSale.cs │ │ ├── FrmStockQuery.cs │ │ └── FrmWarning.cs │ └── Program.cs ├── MedicineSale.BLL // 业务层,放销售开单、采购入库这类动作 │ ├── SaleManager.cs │ ├── StockManager.cs │ └── WarningManager.cs ├── MedicineSale.DAL // 数据访问层,只做增删改查 │ ├── SqlHelper.cs │ ├── MedicineDao.cs │ └── SaleOrderDao.cs └── MedicineSale.Model // 实体类,对应数据库表 ├── Medicine.cs ├── MedicineBatch.cs └── SaleOrder.cs分层时最重要的一条纪律:窗体代码里不要出现 SQL 字符串,DAL 里不要弹 MessageBox,BLL 夹在中间负责「先查库存再决定能不能卖」。查询岗位想查某批药还有多少,走 BLL 的 StockManager,不直接 new SqlConnection。这条纪律能让后面所有改动都只动一个文件。
3.2 连接字符串与 SqlHelper:一个类的增删改查日子
连接字符串放在 UI 层的 App.config 里,而不是硬编码在代码中。以下是可用的最小配置:
<connectionStrings> <add name="MedicineSaleDB" connectionString="Data Source=.;Initial Catalog=MedicineSaleDB;User ID=sa;Password=yourpassword;MultipleActiveResultSets=true" providerName="System.Data.SqlClient"/> </connectionStrings>连接参数说明:Data Source=.表示本机默认实例,如果 SQL Server 是命名实例,要写成localhost\SQLEXPRESS;MultipleActiveResultSets=true允许多个 SqlDataReader 在同一连接上并行工作,打开多个子窗体查询时能少报奇怪的连接占用错误。
数据访问层我习惯封一个 SqlHelper 静态类,把连接打开关闭、参数替换这些重复代码收拢:
public static class SqlHelper { private static readonly string connStr = ConfigurationManager.ConnectionStrings["MedicineSaleDB"].ConnectionString; // 查询,返回 DataTable public static DataTable ExecuteDataTable(string sql, params SqlParameter[] parameters) { using (SqlConnection conn = new SqlConnection(connStr)) 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); // 非连接式获取结果,DataTable 用完即可释放连接 return dt; } } // 增删改,返回受影响行数 public static int ExecuteNonQuery(string sql, params SqlParameter[] parameters) { using (SqlConnection conn = new SqlConnection(connStr)) using (SqlCommand cmd = new SqlCommand(sql, conn)) { cmd.CommandType = CommandType.Text; if (parameters != null) cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteNonQuery(); } } // 取单个值,例如 SELECT COUNT(*) public static object ExecuteScalar(string sql, params SqlParameter[] parameters) { using (SqlConnection conn = new SqlConnection(connStr)) using (SqlCommand cmd = new SqlCommand(sql, conn)) { if (parameters != null) cmd.Parameters.AddRange(parameters); conn.Open(); return cmd.ExecuteScalar(); } } }这套类打磨的必要性在于:所有 DAO 层的查询、更新都走同一个入口,连接字符串只在一个地方配置。遇到过有人把连接串写在每个窗体里,部署时换一台机器改十几个文件,心态直接崩。参数化查询这件事,SqlHelper 也强制了——所有 SQL 都走@参数传值,既防注入,又省去拼字符串时引号来回转义的麻烦。
3.3 用委托解耦:效期预警和库存不足怎么通知界面
这是 C# 里比较实用的一层设计。库存扣减以后,界面上的库存列表要刷新,如果有批次库存低于阈值还要弹提示。如果直接在业务方法里写死「调用某个窗体的刷新方法」,业务层就被 UI 绑架了。
用委托和事件把「发生了什么」和「谁去响应」拆开:
// 业务层只负责发出通知,不关心谁接收 public class StockManager { // 预定义的泛型委托,不需要自己声明 delegate public event Action<string> OnStockWarning; public void DeductStock(int medicineId, int batchId, int quantity) { // 执行 UPDATE 扣减 // 如果扣完后该批次库存低于预警阈值 OnStockWarning?.Invoke($"药品ID {medicineId} 批次 {batchId} 库存不足,请及时补货"); } } // UI 层订阅事件 StockManager stockManager = new StockManager(); stockManager.OnStockWarning += message => MessageBox.Show(message);Action<string>是系统预定义的委托,省掉自己写delegate void StockWarningHandler(string msg)的样板代码。如果希望订阅者能返回值(比如「是否继续执行」),就改用Func<...>。这套机制的收益在效期预警场景特别明显:业务层到点发通知,UI 层想弹窗就弹窗,想做声音提醒就做声音提醒,互不干扰。
4. 销售开单、库存扣减与效期预警:三个必写模块的落地代码
4.1 销售开单与库存扣减:一个事务把首尾绑住
销售开单最少要干三件事:写销售主表、写销售明细、扣批次库存。任何一步失败,其余步骤必须回滚,不然会出现「钱收了但库存没扣」的账面问题。
以下代码展示用 SqlTransaction 把三步绑成一个原子操作:
public bool CreateSaleOrder(SaleOrder order, List<SaleOrderDetail> details) { string sqlOrder = "INSERT INTO SaleOrder(OrderNo, CustomerId, SaleDate, StaffName, TotalAmount, DiscAmount, PayAmount) " + "VALUES(@OrderNo, @CustomerId, GETDATE(), @StaffName, @TotalAmount, @DiscAmount, @PayAmount);" + "SELECT SCOPE_IDENTITY();"; // 取回刚插入的主键 string sqlDetail = "INSERT INTO SaleOrderDetail(OrderId, MedicineId, MedicineName, BatchNo, Spec, Quantity, Price, Amount) " + "VALUES(@OrderId, @MedicineId, @MedicineName, @BatchNo, @Spec, @Quantity, @Price, @Amount);"; string sqlDeduct = "UPDATE MedicineBatch SET Stock = Stock - @Quantity WHERE BatchId = @BatchId AND Stock >= @Quantity;"; using (SqlConnection conn = new SqlConnection(connStr)) { conn.Open(); SqlTransaction tran = conn.BeginTransaction(); try { int orderId; using (SqlCommand cmd = new SqlCommand(sqlOrder, conn, tran)) { cmd.Parameters.AddWithValue("@OrderNo", order.OrderNo); cmd.Parameters.AddWithValue("@CustomerId", order.CustomerId); cmd.Parameters.AddWithValue("@StaffName", order.StaffName); cmd.Parameters.AddWithValue("@TotalAmount", order.TotalAmount); cmd.Parameters.AddWithValue("@DiscAmount", order.DiscAmount); cmd.Parameters.AddWithValue("@PayAmount", order.PayAmount); orderId = Convert.ToInt32(cmd.ExecuteScalar()); } foreach (var d in details) { using (SqlCommand cmd = new SqlCommand(sqlDetail, conn, tran)) { cmd.Parameters.AddWithValue("@OrderId", orderId); // ... 其余明细字段略,写法同上 cmd.ExecuteNonQuery(); } // 扣减对应批次库存,注意 UPDATE 里的 Stock >= @Quantity 条件 using (SqlCommand cmd = new SqlCommand(sqlDeduct, conn, tran)) { cmd.Parameters.AddWithValue("@BatchId", d.BatchId); cmd.Parameters.AddWithValue("@Quantity", d.Quantity); int rows = cmd.ExecuteNonQuery(); if (rows == 0) throw new Exception($"批次 {d.BatchNo} 库存不足"); } } tran.Commit(); return true; } catch { tran.Rollback(); // 任何异常整体回滚 throw; } } }这段代码有两个细节值得抠:一是扣库存的 UPDATE 语句自带Stock >= @Quantity条件,影响行数为 0 就说明库存不够,比「先 SELECT 再判断」更安全,避免并发下两个窗口同时卖同一批货超卖;二是所有 SqlCommand 都显式传入 tran 对象,确保它们都在同一个事务里执行。事务提交前任何一步抛异常,回滚后界面提示错误,库存分文不动。
4.2 近效期预警:阈值放哪,SQL 怎么写
效期预警是医药系统区别于普通进销存的核心功能。常见做法是把阈值做成可配置的参数,界面上让用户填「提前多少天预警」,而不是写死在代码里。
-- @Days 为预警提前天数,业务层传入,例如 90 表示三个月内到期 SELECT m.CommonName, m.Spec, m.Manufacturer, b.BatchNo, b.ProduceDate, b.ExpireDate, b.Stock FROM MedicineBatch b INNER JOIN Medicine m ON b.MedicineId = m.MedicineId WHERE b.ExpireDate BETWEEN GETDATE() AND DATEADD(DAY, @Days, GETDATE()) AND b.Stock > 0 ORDER BY b.ExpireDate ASC; -- 最紧迫的先显示参数说明:@Days是 SqlParameter,传 90 就是查未来三个月到期的批次;GETDATE()取数据库服务器当前时间而不是程序本地时间,防止客户端系统时间不准导致预警失真。b.Stock > 0条件把已售罄批次过滤掉——空库存的批次预警没有业务意义,只会干扰视线。
实际使用中,这个查询结果不要只弹一个窗体,最好导出成表格或打印出来,每周一早上让库管照着核查货架。后面的第 6 章会讲怎么把它做成固定报表。
4.3 先进先出:批次扣减顺序的排序规则
同一个药有多个批次时,卖货要按「到期日最早」的顺序扣减,否则会出现「新批号卖光了,老批号压在角落过期」的经典事故。
批次选择的核心就是一句带顺序的查询:
public DataTable GetFifoBatch(int medicineId) { string sql = "SELECT TOP 10 BatchId, BatchNo, ExpireDate, Stock " + "FROM MedicineBatch " + "WHERE MedicineId = @MedicineId AND Stock > 0 " + "ORDER BY ExpireDate ASC, ProduceDate ASC;"; using (SqlCommand cmd = new SqlCommand(sql)) { cmd.Parameters.AddWithValue("@MedicineId", medicineId); return SqlHelper.ExecuteDataTable(cmd); } }排序规则的参数含义:ORDER BY ExpireDate ASC把最早到期的批次排在最前;当两个批次同一天到期时,再用ProduceDate ASC决定先出生产日期更早的那批,符合通常的先进先出原则。界面下拉框绑定这个查询结果后,营业员只需要选药品,批次由程序自动挑出来,避免人为选错。这也是「系统设计和效期预警互相咬合」需要注意的地方——批次扣减不按效期排,预警做得再漂亮也只是事后补救。
5. 从 zip 到跑通:部署步骤与避坑排查
5.1 部署五步:附加数据库、改连接串、编译运行
拿到 zip 解压后,先别急着双击打开 .sln,按下面顺序来:
- 查看解压目录结构,确认有没有 .sln 解决方案文件、.sql 数据库脚本或 .mdf 数据库文件。如果只有源码而没有数据库文件,就先建库建表;如果 .mdf 和 .ldf 都在,直接走第 2 步。
- 打开 SQL Server Management Studio,右键「数据库」→「附加」→「添加」选择 .mdf 文件,确认 .ldf 同名日志文件能自动匹配。如果附加失败(权限问题见 5.2),就改为直接执行第 1 章中的建库脚本来初始化。
- 打开解决方案,找到 UI 层的 App.config,把连接字符串中的
Initial Catalog确认是刚才附加的数据库名 MedicineSaleDB,User ID和Password改成你的 SQL Server 登录账号。本地开发想省事,可暂时用Integrated Security=True替代账号密码。 - 用 Visual Studio 生成解决方案。如果源码里引用的第三方组件(如某些报表控件)在 NuGet 里没有,删掉对应引用,换成标准库实现,不要为一个小功能卡住整个项目。
- F5 运行,先用管理员账号登录系统,在采购模块录入一笔进货(含批次和效期),再走一遍销售开单验证库存扣减。跑通这两步,系统基本就可以用了。数据库连同程序一起先放在一台机器上跑单机版,确认稳定后再谈多人访问。
5.2 避坑一:附加数据库时「无法打开物理文件 .mdf,操作系统错误 5(拒绝访问)」
现象:SSMS 附加数据库,点击确定后报错,操作系统错误码通常是 5。
原因:SQL Server 服务账号对 .mdf 所在文件夹没有读取权限,这在把数据库文件放在桌面、D 盘根目录时尤其常见。
解决:把 .mdf 和 .ldf 复制到 SQL Server 的默认 Data 目录,一般位于C:\Program Files\Microsoft SQL Server\MSSQLxx.MSSQLSERVER\MSSQL\DATA,在那里再附加一次即可;或者在原目录右键 → 属性 → 安全 → 给相应 SQL Server 服务账号添加完全控制权限。推荐前者,少碰权限设置。
5.3 避坑二:登录失败——连接字符串与实例名对不上
现象:程序启动时直接弹「用户 'sa' 登录失败」或「无法连接到 .」。
原因:SQL Server 安装时默认未必启用了 SQL Server 身份验证模式;账号密码大小写或特殊字符被 XML 转义搞坏;还有可能是 Data Source 写的是.,但本机装的是命名实例,根本没有默认实例。
解决:先用 SSMS 确认能本地登录,再看服务器属性 → 安全性 → 选择「SQL Server 和 Windows 身份验证模式」,改完务必重启 SQL Server 服务。连接字符串这边,强烈建议先拼一个最小测试连接串验证通道,确认通了再逐步加上 MultipleActiveResultSets 等附加参数。
5.4 避坑三:改了数据库没生效——程序跑的可能是旧配置
现象:改了 App.config 里的连接字符串,重新运行,程序连的还是原来的数据库;或者改了表结构,程序查到的还是旧字段。
原因:WinForms 程序运行时读的是bin\Debug\MedicineSale.UI.exe.config,不是项目目录下的 App.config。修改 App.config 后如果没有重新生成,输出目录里的配置文件还是旧的,这属于经典的「黑匣子」问题。
解决:在 Visual Studio 里重新生成整个解决方案,再确认一次生成的 exe 同目录下的 .config 文件内容是不是最新——这是最直接的排查方法。同理,改了代码没生效,先检查是不是没重新编译就直接运行了旧 exe。
5.5 避坑四:并发销售时死锁——事务顺序不一致
现象:系统上线后,几个收银台同时卖同一批药,偶尔出现「事务(进程 ID)与另一个进程已被死锁,请重试该事务」。
原因:两个窗口各开了一个事务,一个先写销售单再扣库存,另一个先扣库存再写销售单,锁的申请顺序相反,数据库检测到死锁后主动杀掉其中一个事务。
解决:在代码层面统一事务内的操作顺序——全部先写主表再写明细最后扣库存,并在扣库存时使用UPDATE ... WHERE Stock >= @Quantity这类条件更新,尽量缩短持锁时间。不要在生产环境盲目把隔离级别升到 SERIALIZABLE,那会让死锁变成常态。
6. 做个能守住效期的聪明系统:三个具体技巧
第一个技巧,把效期台账做成一个持续刷新的视图,而不是每次手工查。视图的好处是它能固定表的 JOIN 关系和过滤条件,业务代码永远只写一句SELECT * FROM vw_ExpiringBatch:
CREATE VIEW vw_ExpiringBatch AS SELECT m.MedicineCode, m.CommonName, m.Spec, b.BatchNo, b.ExpireDate, b.Stock, DATEDIFF(DAY, GETDATE(), b.ExpireDate) AS DaysToExpire FROM MedicineBatch b INNER JOIN Medicine m ON b.MedicineId = m.MedicineId WHERE b.Stock > 0 AND DATEDIFF(DAY, GETDATE(), b.ExpireDate) <= 90;主界面加载时绑定这个视图,每天打开系统第一眼看到的就是 90 天内到期的货。药品过期前 30 天抽屉里锁着,到期当日直接禁售——这两条规则落在代码里加判断,比指望营业员看日期靠谱得多。
第二个技巧,单机版升级局域网多用户版时,整个改造往往只是「把连接字符串的服务器名从.改成数据库服务器的 IP + 实例名」。数据库不用动,程序也不用重写,但要留意两点:一是 SQL Server 要开启 TCP/IP 协议并允许远程连接,二是前面 5.5 节的事务顺序规范这时候能真正发挥价值。等数据量上来、门店多了,再考虑迁移到 C/S 或浏览器端架构,交付风险会小很多。
第三个技巧,数据库备份别依赖手工。SQL Server 里建一个维护计划,每天凌晨做一次完整备份,备份文件保留 15 天;或者用 Windows 任务计划定时调用 sqlcmd 执行 BACKUP DATABASE 语句,半小时的事,换来的是「后悔药」。小药店数据量不大,一份几 MB 的备份文件足以应付最坏情况。
我做医药项目时都会固定做一件事——每周一早上打开效期台账,把未来 90 天到期的单子打印出来,挨个核对货架。卖药的生意,效期不是玄学,是实打实的利润和合规。希望这一套从数据模型到部署避坑的梳理,能帮你在这个系统上少走弯路,把这套骨架真正做成自己的项目。希望帮到你。
本文还有配套的精品资源,点击获取