拓冰建站拓冰建站
首页 / 资讯中心 / 正文

C#医药销售管理系统源码解析:批号效期与数据库设计实战

简介基于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 里不要弹 MessageBoxBLL 夹在中间负责「先查库存再决定能不能卖」。查询岗位想查某批药还有多少走 BLL 的 StockManager不直接 new SqlConnection。这条纪律能让后面所有改动都只动一个文件。3.2 连接字符串与 SqlHelper一个类的增删改查日子连接字符串放在 UI 层的 App.config 里而不是硬编码在代码中。以下是可用的最小配置connectionStrings add nameMedicineSaleDB connectionStringData Source.;Initial CatalogMedicineSaleDB;User IDsa;Passwordyourpassword;MultipleActiveResultSetstrue providerNameSystem.Data.SqlClient/ /connectionStrings连接参数说明Data Source.表示本机默认实例如果 SQL Server 是命名实例要写成localhost\SQLEXPRESSMultipleActiveResultSetstrue允许多个 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 Actionstring 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);Actionstring是系统预定义的委托省掉自己写delegate void StockWarningHandler(string msg)的样板代码。如果希望订阅者能返回值比如「是否继续执行」就改用Func...。这套机制的收益在效期预警场景特别明显业务层到点发通知UI 层想弹窗就弹窗想做声音提醒就做声音提醒互不干扰。4. 销售开单、库存扣减与效期预警三个必写模块的落地代码4.1 销售开单与库存扣减一个事务把首尾绑住销售开单最少要干三件事写销售主表、写销售明细、扣批次库存。任何一步失败其余步骤必须回滚不然会出现「钱收了但库存没扣」的账面问题。以下代码展示用 SqlTransaction 把三步绑成一个原子操作public bool CreateSaleOrder(SaleOrder order, ListSaleOrderDetail 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确认是刚才附加的数据库名 MedicineSaleDBUser ID和Password改成你的 SQL Server 登录账号。本地开发想省事可暂时用Integrated SecurityTrue替代账号密码。用 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_ExpiringBatchCREATE 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 天到期的单子打印出来挨个核对货架。卖药的生意效期不是玄学是实打实的利润和合规。希望这一套从数据模型到部署避坑的梳理能帮你在这个系统上少走弯路把这套骨架真正做成自己的项目。希望帮到你。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门