☰
数据库课程设计实战:药品管理系统从需求分析到触发器全流程
2026/10/14 11:19:13 网站建设 项目流程

简介:一份完整的数据库课程设计报告,围绕药品管理系统展开,面向高校计算机相关专业学生及需要完成数据库课程设计的开发者。报告以 SQL Server 为平台,基于真实业务流程完成从需求分析到系统实施的全程设计,可用作数据库原理、SQL 语言和系统设计相关课程的参考范例。资源包仅 1 个 doc 文档,整体大小 1.07MB,便于直接下载阅读。文档结构清晰,包含需求分析、业务流程图、数据字典、概念设计 E-R 图、逻辑与物理设计、数据库实施及系统测试等核心章节,内容覆盖数据库设计全流程,能够帮助读者理解如何将数据库理论应用于药品信息管理场景。该报告对药品信息存储、查询、更新与删除等操作的实现思路做了较完整记录,适合作为课程设计报告写作、答辩准备和数据库项目开发的参考资料。目前已有 1037 人学习下载,对于需要快速搭建课程设计框架或参考规范文档结构的读者有较好的借鉴价值。

1. 数据库课程设计别急着建表:药品管理系统的完整套路

刚拿到“数据库课程设计”这个题目时,很多人第一反应是打开 SQL Server 建两张表往里塞数据。真要这么干,多半会在需求分析环节就被打回来。这份药品管理系统课程设计报告,完整走了一遍需求分析、概念设计、逻辑设计、物理设计、实施、运行维护的标准流程,数据库部分从业务流程图和数据字典起步,一路落到建库 SQL、视图、触发器和存储过程,是一份可以直接照抄架构的课程设计蓝本。适合正在做数据库课程设计或毕业设计、需要一套完整设计文档的人,也适合想搞明白 SQL Server 里 check 约束、视图、触发器、存储过程是怎么配合使用的初学者。

2. 需求分析与概念设计:先讲清三个业务流,再定 25 个数据项

2.1 需求分析不是写前言,是把三个业务流画出来

需求分析阶段最忌讳直接写“随着信息技术的发展”这种套话。药品管理系统这门课设的正确打开方式是先做业务梳理,再画业务流程图。项目里把药店的实际运作拆成了三个业务:药品购进业务、药品出售业务、药品存储业务。

药品购进业务的完整链路是:购药人员根据卖药处的需求以及库存信息,制定药品需求单据,从制药商处购药;药品合格则连同单据一起交给库存管理员入库,不合格则退回制药商。存药业务是库存管理员把购得的药品分类入库,接到取药单或退药单时执行出库或重新入库,同时修改库存信息;如果发现缺货,把缺货药品信息单递给购药人员。售药业务则是买药人拿取药单或退药单到售药处,确认后将药品售出或退回,售药处再把单据转给库存管理员。

这三条业务流是后面所有设计的源头。实体怎么拆分、联系表建几张、触发器怎么写,都由这三个业务决定。比如“药品购进”会引出 DOrder 和 OBack 两张订单表,“售药”会引出 DBuy 和 BBack 两张交易表,“存储”会引出 Stored 表和库存加减逻辑。画业务流程图时,不用追求标准建模工具的精细,重点是保证每个业务都有明确的起点、终点和参与者,图能自圆其说就够了。

2.2 数据字典:25 个数据项和 8 个数据结构是后面所有表的“契约”

需求分析的结果要落到数据字典上。数据字典包含数据项、数据结构、数据存储和处理过程四部分,核心作用是定义“数据流”和“数据存储”的逻辑内容。这份文档共定义了 25 个数据项,最值得抄的是它的约束设计逻辑。

数据项存储结构约束/业务含义
Dnochar(5)药品编号,主键
Dnamechar(20)药品名称,not null
price1 / price2float进价/售价,check 大于零
Pnochar(5)买药人编号,主键
Pageint年龄,check 1 到 120
Psexchar(2)性别,check 男/女
Mnochar(5)制药商编号,主键
Lnochar(5)柜台编号,主键
Quantityint药品数量,check 大于零
Supplychar(4)订退方式,check 订购/退订
Dealchar(4)售退方式,check 售出/退回
ID / Passwordchar(4) / char(6)系统用户登录,Password 非空

这里要特别说两件事。第一,枚举型字段用 char(4) 而不是 varchar 是课程设计里的聪明写法——长度和取值域都由 check 卡死,答辩时可以说“这样能避免脏数据”。第二,每个主键都用 char(5) 或 number(5) 定长,没有设自增列。定长主键在书籍、药品这类有天然编码规则的场景里反而更好用,比如 Dno 可以直接编成“D1001”。如果用了 int 自增,后面做视图和触发器的连表查询一样能跑,但数据字典阶段就要先定义好编号生成规则。

数据结构一共 8 个:Drug(药品)、Maker(制药商)、Patient(买药人)、Storage(柜台)、Order_Back(药品退订)、Buy_Back(药品售退)、Stored(药品存储)、User(系统用户)。注意原文数据结构里 Order_Back 和 Buy_Back 各是一个结构,但建表阶段拆成了 DOrder / OBack、DBuy / BBack 四张表。这么拆的目的是把“订购”和“退订”两个动作分表存储,虽然冗余了一点,但触发器的库存加减逻辑会变得非常直观。

2.3 从分 E-R 图到全局 E-R 图:把“联系”识别准确

概念设计阶段的输入是需求分析的用户需求,输出是 E-R 图。实体集在上面已经列过:系统用户、药品、买药人员、制药商、柜台信息,以及药品退订、药品售退、药品存储这几组带属性的联系。给实体定义属性时,原文非常克制:药品只放了编号、名称、分类、规格、品牌、进价、售价,没有把生产日期、有效期、批次放进来。药品有效期和批次管理对真实药店是刚需,但在这个课程设计里会显著增加复杂度,省略是合理的题目取舍。

分 E-R 图按子系统切分。药品存储分 E-R 图围绕“药品—柜台”的多对多联系展开,属性是存储数量;药品售出分 E-R 图围绕“买药人—药品”的多对多联系展开,属性是药品数量、处理时间、售退方式;药品退订分 E-R 图围绕“药品—制药商”的多对多联系展开。合并成全局 E-R 图时要处理三类冲突:属性冲突(同名属性类型或取值域不一致)、命名冲突(同名异物、异名同物)、结构冲突(同一对象在一处是实体、另一处是属性)。

初学者最常犯的错是把多对多联系画成一个孤立的实体。正确的做法是让联系带上自身属性,在逻辑设计阶段自然转成一张独立表。比如“药品—买药人”的联系带数量、时间、售退方式,转成 DBuy/BBack 表;“药品—制药商”的联系带数量、时间、订退方式,转成 DOrder/OBack 表。E-R 图这一步画对了,后面建表就是翻译工作。

3. 逻辑设计与物理设计:把 E-R 图翻译成表、约束和索引

3.1 关系模式设计:10 张表是怎么拆出来的

逻辑设计的任务是把 E-R 图转换为所选 DBMS 支持的关系模式。实体转表,多对多联系转独立表,一对多联系通常把一端的码并入多端。按这个规则,最终建了 10 张表:

表名用途主键
User系统用户登录ID
Drug药品基本信息Dno
Patient买药人信息Pno
Maker制药商信息Mno
Storage柜台信息Lno
DOrder药品订购Mno, Dno
OBack药品退订Mno, Dno
DBuy药品售出Pno, Dno
BBack药品退回Pno, Dno
Stored药品存储Lno, Dno

仔细观察会发现,原文的需求分析里写的是 Order_Back、Buy_Back、Store_Drug,但建表阶段用的是 DOrder、OBack、DBuy、BBack、Stored。这个命名不一致在课程设计报告里很常见,答辩老师会盯着问,建议写报告时统一成一组命名,代码里用什么,文档里就写什么。

拆分逻辑值得说两句。药品订购和退订是两类方向相反的操作,原文分别建了 DOrder 和 OBack,用 Supply 字段(订购/退订)进一步约束。药品售出和药品退回同理,拆成 DBuy 和 BBack。这样做的直接好处是写触发器时减库存、加库存非常清楚——订购加库存、退订减库存、售出减库存、退回加库存,四条规则各对一张表。代价是表和触发器的数量翻倍,数据字典也从 8 个数据结构膨胀到 10 张表。对课程设计来说,这个取舍是值得的。

外键关系也把业务串起来了:DOrder/OBack 的 Mno 引用 Maker,Dno 引用 Drug;DBuy/BBack 的 Pno 引用 Patient,Dno 引用 Drug;Stored 的 Lno 引用 Storage,Dno 引用 Drug。这里有个设计细节:Stored 的主键是 (Lno, Dno),但物理设计阶段给它建的唯一索引却是 (Dno, Lno),两个字段顺序相反。对查询“某药品在哪些柜台”和“某柜台有哪些药品”两种场景,前者按药品维度定位,后者按柜台维度定位。原文在物理设计里选了按药品维度建索引,说明这个系统的核心查询是“药品定位”,而不是“柜台盘点”。

3.2 完整性约束:check 约束把取值域卡死在 DDL 里

完整性约束分三类,原文全部落实了。实体完整性通过主键实现,每个表都有明确的主键,且 DOrder、OBack、DBuy、BBack、Stored 五个多对多关系表用的是复合主键。参照完整性通过外键实现,所有引用关系的字段都加了 foreign key。用户自定义完整性通过 check 约束实现,这是最有“数据库味”的部分:

  • 进价 price1 > 0、售价 price2 > 0
  • 年龄 1 到 120
  • 性别只能是男或女
  • 数量大于零
  • Supply 只能是订购或退订(原文 DOrder 表 check 写的是 Supply='订购',OBack 表 check 写的是 Supply='退订',等于让字段名和一个固定值绑定)
  • Deal 只能是售出或退回

把取值域逻辑写进 DDL,而不是依赖应用层校验,这是数据库课程设计的正确姿势。缺点也直说:如果系统后面要支持“预售”“调拨”这类新状态,改 check 约束比改应用层逻辑麻烦得多。课程设计阶段不需要考虑这种扩展性,优先把完整性写死。

安全性部分原文用了两层机制。一层是视图机制,不同用户只能访问系统授权的视图,比如买药人视图和管理员视图;另一层是用户授权机制,通过 User 表登录识别用户级别,再按级别分配权限。User 表设计得比较朴素,只有 ID 和 Password 两个字段,没有加密存储、没有角色表。真实系统里密码至少要哈希存储,但课程设计能说清“按用户级别分配权限”这个概念就够用了。

3.3 物理设计:索引选在哪几个表上,是拍脑袋还是按查询路径

物理设计要回答“数据怎么存取最快”。原文选的是索引存取方法,只建了三个唯一性索引:

  • Stored(Dno, Lno):存储表主码两个字段经常出现在查询条件里,且更新频率低
  • DOrder(Mno, Dno):订单表主码取值唯一,常被查询
  • DBuy(Pno, Dno):买药人-药品关系主码唯一,常被查询

单独看这三个索引会觉得少,但这符合课程设计的体量——10 张表、几百条数据,建太多索引反而拖慢写入。索引选择的两个原则是:一、经常在 WHERE 和 JOIN 条件里出现的字段才建索引;二、更新频率低、取值唯一的字段适合唯一索引。Stored 和 DBuy 这种关系表会在触发器里被频繁更新,所以原文只对主码建索引,不对 Quantity 这类被 update 的字段建,是合理的。

物理设计还要评价时间和空间效率。课程设计里这一节容易被当成凑字数,其实一句话就能说清:药品表数据量最大、查询最频繁,Drug 和 Patient 主键各建了唯一索引;柜台和制药商表数据量小、更新少,靠主键默认索引就够了。至于存储过程的效率评价,把三个按时间查询的过程直接写出来并在试运行阶段跑通,比空谈“效率很高”更有说服力。

4. 实施阶段动手:从建库到触发器、存储过程的完整代码

4.1 建库建表:把所有约束一次写进 DDL

实施阶段第一步是在 SQL Server 里建库建表。建库语句很简单:

create database DrugStore;

建表要按“先父表后子表”的顺序执行,否则外键引用的表还不存在,会直接报错。先建 User、Drug、Patient、Maker、Storage,再建依赖它们的 DOrder、OBack、DBuy、BBack、Stored。

-- 系统用户表 create table [User]( ID char(4) primary key, Password char(6) not null ); -- 药品表:主键 + 非空 + 进价售价大于零 create table Drug( Dno char(5) primary key, Dname char(20) not null, Dclass char(8), Dguige char(10), Dbrand char(10), price1 float, price2 float, check(price1 > 0), check(price2 > 0) ); -- 买药人表:年龄 1-120,性别男/女,电话非空 create table Patient( Pno char(5) primary key, Pname char(10) not null, Page int, Psex char(2), Pphone char(11) not null, Paddress char(20), check(Page >= 1 and Page <= 120), check(Psex = '男' or Psex = '女') ); -- 制药商表 create table Maker( Mno char(5) primary key, Mname char(30) not null, Mplace char(10) not null, Mphone char(20) not null ); -- 柜台表 create table Storage( Lno char(5) primary key, Lname char(20) not null );

这里有个明显的坑:User 是 SQL Server 的保留字,直接 create table user 会报语法错误,所以我在上面写成 [User]。原文附录里的 DDL 直接写 create table user,在 SQL Server 2000 里是会翻车的,这是课程设计报告最常见的“代码没跑过”痕迹。

外键表的建表语句长一些,但逻辑是一致的:复合主键 + 外键 + check 约束:

-- 药品订购表:制药商订药品,入库加库存 create table DOrder( Mno char(5) not null, Dno char(5) not null, Quantity int not null, Time_SD datetime, Supply char(4) not null, primary key(Mno, Dno), foreign key(Mno) references Maker(Mno), foreign key(Dno) references Drug(Dno), check(Quantity > 0), check(Supply = '订购') ); -- 药品退订表 create table OBack( Mno char(5) not null, Dno char(5) not null, Quantity int not null, Time_SD datetime, Supply char(4) not null, primary key(Mno, Dno), foreign key(Mno) references Maker(Mno), foreign key(Dno) references Drug(Dno), check(Quantity > 0), check(Supply = '退订') ); -- 药品售出表:买药人买药,减库存 create table DBuy( Pno char(5) not null, Dno char(5) not null, Time_SD datetime, Quantity int not null, Deal char(4) not null, primary key(Pno, Dno), foreign key(Pno) references Patient(Pno), foreign key(Dno) references Drug(Dno), check(Quantity > 0), check(Deal = '售出') ); -- 药品退回表 create table BBack( Pno char(5) not null, Dno char(5) not null, Time_SD datetime, Quantity int not null, Deal char(4) not null, primary key(Pno, Dno), foreign key(Pno) references Patient(Pno), foreign key(Dno) references Drug(Dno), check(Quantity > 0), check(Deal = '退回') );

我在原文基础上做了一处调整:原文的 DOrder/OBack/DBuy/BBack 复合主键是 (Mno,Dno) 或 (Pno,Dno),但没把 Time_SD 纳入主键。这带来的问题是:同一个人同一天两次买同一种药,会违反主键约束。更稳的做法是把主键改成 (Pno, Dno, Time_SD),或者加一个自增订单号。课程设计报告里不改也能过,但答辩时被问到“重复购药怎么办”就露怯了。

最后是存储关系表,库存由触发器维护,建表时只定义结构:

-- 药品存储表:药品和柜台的对应关系 create table Stored( Dno char(5) not null, Lno char(5) not null, Quantity int not null, primary key(Lno, Dno), foreign key(Lno) references Storage(Lno), foreign key(Dno) references Drug(Dno), check(Quantity > 0) );

Stored 的主键是 (Lno, Dno),先柜台后药品,语义是“一个柜台存多种药品”。如果设计侧重“一种药品分布在多个柜台”,主键顺序可以反过来。物理设计里的唯一索引用的是 (Dno, Lno),正好覆盖了按药品查柜台的查询路径,这个组合可以讲给答辩老师听。

4.2 视图:按角色切数据口径,安全性靠视图挡住第一层

视图在系统里承担两个职责:一是把多表 JOIN 的复杂查询固定成“一张虚拟表”,让前端查询简单;二是做安全性控制,不同用户只能看授权视图,看不到底层表的完整结构。原文建了四个视图:

-- 买药人对药品的查询:药品名字、规格、品牌、制药商 create view DM_P as select Dname 药品名字, Dguige 规格, Dbrand 品牌, Mname 制药商名称, Mplace 产地, Mphone 联系电话 from Drug, Maker, DOrder where Drug.Dno = DOrder.Dno and Maker.Mno = DOrder.Mno with check option; -- 管理员对药品的查询:多了进价售价分类 create view DM_M as select Dname, Dclass, Dguige, Dbrand, price1, price2, Mname, Mplace, Mphone from Drug, Maker, OBack where Maker.Mno = OBack.Mno and Drug.Dno = OBack.Dno with check option; -- 管理员对病人信息及买药情况的查询 create view PD_M as select Pname, Page, Psex, Pphone, Dname, price1, price2, Quantity, Time_SD from Drug, Patient, BBack where Drug.Dno = BBack.Dno and Patient.Pno = BBack.Pno with check option; -- 管理员对柜台药品的查询 create view DS_M as select Drug.Dno, Dname, price1, price2, Lname, Quantity from Drug, Stored, Storage where Drug.Dno = Stored.Dno and Storage.Lno = Stored.Lno with check option;

注意 DS_M 的 join 条件,原文写的是 Drug.Dno = Stored.Lno,这是明显的笔误,应该改成 Stored.Dno。我在上面直接修正了。四个视图的职责分得很清楚:DM_P 是买药人视角,不暴露进价;DM_M 是管理员视角,带进价和售价;PD_M 把病人信息和购药记录关联起来,方便召回和退药核实;DS_M 是柜台库存视角。四个视图把系统里最常查的四类数据提前切好了,应用层不需要写一堆 join。

with check option 的作用是对视图做插入或更新时,强制数据满足视图的过滤条件。比如往 DM_P 里插数据,必须同时满足 DOrder 里的外键关系,这层保护在课程设计里加分明显。

4.3 触发器:把库存账自动带上,别让数据手改

触发器是这份设计里最有含金量的部分。库存数量不该靠人肉 update,而应该由业务表自动驱动。原文建了 5 个触发器:

-- 订购入库:库存加 create trigger DOrder_insert on DOrder after insert as update Stored set Stored.Quantity = Stored.Quantity + inserted.Quantity from Stored, inserted where Stored.Dno = inserted.Dno; -- 退订出库:库存减 create trigger OBack_insert on OBack after insert as update Stored set Stored.Quantity = Stored.Quantity - inserted.Quantity from Stored, inserted where Stored.Dno = inserted.Dno; -- 售出出库:库存减 create trigger DBuy_insert on DBuy after insert as update Stored set Stored.Quantity = Stored.Quantity - inserted.Quantity from Stored, inserted where Stored.Dno = inserted.Dno; -- 退回入库:库存加 create trigger BBack_insert on BBack after insert as update Stored set Stored.Quantity = Stored.Quantity + inserted.Quantity from Stored, inserted where Stored.Dno = inserted.Dno;

四个触发器两两对称,方向相反,逻辑非常清晰。inserted 是 SQL Server 触发器里的虚拟表,包含刚插入的新数据;更新 Stored 时用 Stored.Dno = inserted.Dno 匹配到对应药品的库存行。每个触发器都是 after insert,意味着先写入订单表,再联动库存表,保证业务数据先落地。

提示:同一订单里如果包含两种药品,inserted 里会有两行,触发器会对 Stored 表各更新一次,这个没问题;但如果是同一种药品同批次订购两条记录,inserted 里有两行同 Dno,update 语句会按 Dno 匹配后执行两遍,库存会被加两次。

解决的办法是把 update 改成先按 Dno 汇总再执行:

update Stored set Quantity = Quantity + t.qty from Stored, (select Dno, sum(Quantity) qty from inserted group by Dno) t where Stored.Dno = t.Dno;

第五个触发器是缺货提示:

-- 缺货报警:库存低于 10 打印提示 create trigger Stored_quohuo on Stored after insert, update as select Dno, Quantity from Stored where Stored.Quantity < 10 print('药品货不足');

这个触发器在 Stored 被 insert 或 update 后触发,检查所有库存小于 10 的药品并打印提示。print 只能输出到消息窗口,应用层不一定能捕获,真实系统里应该改成往缺货表插记录或发消息,课程设计有 print 就够体现了。注意触发器的时序:如果前面四个库存触发器先执行完,Stored_quohuo 再触发,就能看到真实的库存结果;如果它先触发,查到的可能是旧值。SQL Server 对同一表上的多个 after 触发器不保证执行顺序,稳妥做法是把它和库存联动触发器合并,或者显式指定 sp_settriggerorder。

4.4 存储过程:把常用操作折叠成接口

存储过程相当于给表操作套了一层函数接口,前端或应用层调用时不用拼冗长的 insert 语句。原文定义了 7 个存储过程,分两类:四个插入、三个时间查询。

-- 药品插入存储过程:参数与 Drug 字段一一对应 create procedure Drug_insert @drug_no char(5), @drug_name char(20), @drug_class char(8), @drug_guige char(10), @drug_brand char(10), @drug_price1 float, @drug_price2 float as insert into Drug values(@drug_no, @drug_name, @drug_class, @drug_guige, @drug_brand, @drug_price1, @drug_price2); -- 买药人插入存储过程 create procedure Patient_insert @patient_no char(5), @patient_name char(10), @patient_age int, @patient_sex char(2), @patient_phone char(11), @patient_address char(20) as insert into Patient values(@patient_no, @patient_name, @patient_age, @patient_sex, @patient_phone, @patient_address); -- 制药商插入存储过程 create procedure Maker_insert @maker_no char(5), @maker_name char(20), @maker_place char(20), @maker_phone char(11) as insert into Maker values(@maker_no, @maker_name, @maker_place, @maker_phone); -- 柜台插入存储过程 create procedure Storage_insert @storage_no char(5), @storage_name char(20) as insert into Storage values(@storage_no, @storage_name);

四个插入过程都是标准的“参数→insert”模式。参数名里带 @,是 T-SQL 局部变量的写法;参数类型和长度要和表定义保持一致,比如 Patient_insert 的 @patient_phone 是 char(11),如果应用层传了一个 12 位的手机号,会直接截断。这是课程设计里最容易忽略的地方。

时间查询存储过程是面向“某一天卖了多少药、退了多少药、订了多少药”这类运营统计场景的:

-- 按时间查询售出记录 create procedure DBuy_Time_select @dbt_time datetime as select Pno, Dno, Quantity from DBuy where Time_SD = @dbt_time; -- 按时间查询退回记录 create procedure BBack_Time_select @bbt_time datetime as select Pno, Dno, Quantity from BBack where Time_SD = @bbt_time; -- 按时间查询订购记录 create procedure DOrder_Time_select @dot_time datetime as select Mno, Dno, Quantity from DOrder where Time_SD = @dot_time;

调用方式:

exec DBuy_Time_select @dbt_time = '2024-06-01 10:00:00';

注意原文的 BBack_Time_select 里写的是 from Buy_Back,而建表名是 BBack,SQL Server 找不到 Buy_Back 这个表,会直接报“对象名无效”。这是原报告中一个典型的“文档和代码不一致”问题,后面避坑章还会展开。

5. 避坑清单:课程设计里最常见的五个翻车点

课程设计这套流程,理论上照着走都能跑通,但实际操作时总有几个位置固定翻车。下面这五条,是我对照原报告和实际执行情况整理出来的高频问题,每一条都按“现象 → 原因 → 解决”写清楚。

5.1 create table user 报语法错误

现象:执行建表语句时,SQL Server 直接在 create table user 这一行报错,错误信息类似“'user' 附近有语法错误”。

原因:USER 是 SQL Server 的保留字,用于 USER_NAME() 这类函数和系统上下文,不能直接做表名。

解决:给表名加方括号写成 [User],或者直接把表改名成 SysUser,和字段 ID、Password 一起改成规范命名。课程设计文档里如果写的是 create table user,说明这段 DDL 并没有在 SQL Server 里实际执行过,答辩时老师一眼就能看出来。

5.2 BBack_Time_select 存储过程执行报“对象名无效”

现象:exec BBack_Time_select 时报错,找不到要查询的对象。

原因:建表时表名是 BBack,存储过程里 select 的却是 Buy_Back;这是文档里表格、数据结构命名和实际建表名不一致的典型表现。

解决:把存储过程里的 from Buy_Back 改成 from BBack,并在文档图表里全局统一命名。经验是:写完整份报告后,把附录里的所有表名整理成一张清单,逐个和 create table 语句比对。

5.3 视图 DS_M 查询结果错乱但 SQL 不报错

现象:DS_M 视图查询出来药品和柜台对应关系完全乱掉,比如某药品出现在它从未存储过的柜台下,但 SQL 没有任何报错。

原因:视图 join 条件写错了,原文写的是 Drug.Dno = Stored.Lno,等于拿药品编号去对柜台编号,两个不同业务含义的字段硬凑在一起,产生了错误连接后的脏数据。

解决:把条件改成 Drug.Dno = Stored.Dno,同时检查另一个条件 Storage.Lno = Stored.Lno。这个坑最坑的地方是“不报错”,结果错了还让人以为自己是数据问题。排错时可以先单表查 Stored 和 Storage 的关联,再逐步加 join 条件定位。

5.4 触发器只覆盖 insert,修改历史订单后库存不联动

现象:往 DOrder 表插入数据时库存会变,但把某条已存在的订单数量从 10 改成 15,Stored 里的库存纹丝不动。

原因:四个库存触发器全部是 after insert,只响应插入事件;update 订单时没有任何东西去改 Stored。

解决:给 DOrder、OBack、DBuy、BBack 各补一个 after update 触发器,或者在存储过程里统一处理“修改订单 → 重算库存”的逻辑。真实系统里更稳妥的做法是,库存变化全部走存储过程,禁止直接对订单表执行 update/delete,从源头避免漏触发。

5.5 复合主键没带时间,重复购药直接报主键冲突

现象:同一个买药人同一天两次购买同一种药品,第二次插入 DBuy 表时报主键冲突。

原因:DBuy 的主键是 (Pno, Dno),没有把 Time_SD 纳入,同一对 (Pno, Dno) 只能出现一次。

解决:把主键改成 (Pno, Dno, Time_SD),或者增加自增订单号 OrderID 做主键。顺带说一句,原文里 DOrder 和 OBack 的复合主键是 (Mno, Dno),同样存在同一个制药商对同一种药品既订购又退订时冲突的问题——订购插 DOrder、退订插 OBack,因为分成两张表所以没冲突,但如果哪天把两张表合并成一张用 Supply 区分,主键就必须带上 Time_SD。

这五条如果全部避掉,这份课程设计的实施部分基本不会有大问题。剩下的就是试运行环节多造点真实数据,把触发器、视图、存储过程轮着跑一遍,而不是只验证“能 select”。

6. 验证方法:用触发器联动和订单时间查询收尾

代码写完不是终点,课程设计的最后一道坎是“能演示”。答辩时老师不会只问“你建了几张表”,而是会丢几个操作让你现场跑:进一批货、卖一批药、退一批药,然后看库存对不对,再看某天的订单能不能查出来。这一套流程其实就是验证触发器和存储过程的最好手段。

我的习惯是准备一组最小测试数据,按业务顺序执行一遍:

-- 1. 建基础数据 exec Drug_insert @drug_no='D1001', @drug_name='感冒灵', @drug_class='中成药', @drug_guige='10袋', @drug_brand='999', @drug_price1=8.5, @drug_price2=15.0; exec Storage_insert @storage_no='L001', @storage_name='A 柜'; exec Maker_insert @maker_no='M001', @maker_name='某制药厂', @maker_place='北京', @maker_phone='01012345678'; exec Patient_insert @patient_no='P001', @patient_name='张三', @patient_age=35, @patient_sex='男', @patient_phone='13812345678', @patient_address='某街道 1 号'; -- 2. 先做存储关系,再看初始库存 insert into Stored(Dno, Lno, Quantity) values('D1001', 'L001', 50); select * from Stored where Dno='D1001'; -- 3. 订购 20 盒,触发 DOrder_insert,库存应变 70 insert into DOrder(Mno, Dno, Quantity, Time_SD, Supply) values('M001', 'D1001', 20, '2024-06-01 10:00:00', '订购'); select * from Stored where Dno='D1001'; -- 4. 售出 5 盒,触发 DBuy_insert,库存应变 65 insert into DBuy(Pno, Dno, Time_SD, Quantity, Deal) values('P001', 'D1001', '2024-06-01 11:00:00', 5, '售出'); select * from Stored where Dno='D1001'; -- 5. 按时间查当天售出记录 exec DBuy_Time_select @dbt_time='2024-06-01 11:00:00';

第 3、4 步的两次 select * from Stored 是关键见证:如果第二次执行后库存不是 70,第三次执行后不是 65,说明触发器没生效,优先检查 inserted 表的引用方式和 Dno 匹配条件。这里还有个容易混淆的点:订购加库存、售出减库存,方向一定不能写反,否则库存会越卖越多。

答辩演示时还可以顺手证明视图的安全性:用普通买药人账号只能查 DM_P,看不到进价;管理员账号才能查 DM_M。这个对照演示 10 秒钟就能完成,但能非常直观地证明“视图机制 + 用户授权”这部分设计不是空话。

从那以后,我每次做课程设计验收,都强制自己走一遍“新增基础数据 → 触发一次库存联动 → 按时间查一次订单”的完整链路,再检查存储过程调用是否全部成功,最后才敢写“调试与试运行通过”。这套流程看着笨,但确实能挡住绝大多数翻车。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询