简介:本资源是一份面向高校数据库课程初学者的完整课程设计报告,聚焦药品管理场景,解决小型机构人工管理效率低、易出错的现实问题。报告基于SQL Server数据库平台,系统覆盖需求分析(含业务流程图与数据字典)、概念设计(局部与全局E-R图)、逻辑设计(关系模式与约束定义)及物理与实施阶段要点,内容结构严谨、步骤清晰,适合作为数据库原理与应用课程的实践参考范本。资源为单个Word文档(.doc格式),文件大小1.07MB,内容完整呈现从需求建模到逻辑建模的全过程,目录层级分明,含摘要、四大设计阶段详述及收获体会等模块,便于对照学习与课程作业借鉴。目前已有1037人学习下载,读者可直接获取规范的数据库设计方法论、标准文档撰写框架及典型医药管理领域的建模思路。
1. 这不是一份“交差式”课程报告:它是一套能跑通的 SQL Server 药品管理最小可行系统(含完整建库脚本、触发器逻辑、视图权限控制与真实业务约束)
你手头这份《数据库课程设计报告-药品管理系统.doc》,表面看是学生交作业用的 Word 文档,但拆开附录里的 SQL 脚本、E-R 图逻辑和数据字典定义,它其实是一套可落地、有边界、带业务语义的 SQL Server 2000 实战模板——不是伪代码,不是示意表,而是当年真在机房里敲出来、连上 SSMS 能查能插能触发库存自动更新的完整链路。它解决的不是“数据库原理考题”,而是小型药店最痛的三个现实问题:进药后库存没同步、退药时数量算错、查畅销药要翻三张表手工加总。整套设计严格遵循“需求→E-R→关系模式→索引→触发器→视图→存储过程”工业级流程,所有 CHECK 约束(如Page>=1 and Page<=120)、外键引用(foreign key(Dno) references Drug(Dno))、唯一索引(create unique index DLno on Stored(Dno,Lno))全部写死在 SQL 里,没有一句空话。如果你正被课程设计卡在“建不出能动的库”“触发器总报错”“视图查不出数据”上,或者想快速复现一个带真实业务闭环的数据库教学案例——这份文档就是你该抄的作业。它不炫技,但每行 SQL 都踩过坑;它用的是 SQL Server 2000(老但稳),但所有逻辑平移至 2012/2019/2022 仅需微调数据类型(如number(5)→char(5))和语法(如print改为RAISERROR)。别把它当论文读,把它当源码包拆。
2. 从 E-R 图到 9 张物理表:如何把“药品购进/售出/退订”业务流翻译成不可篡改的关系模式
2.1 为什么必须先画清分 E-R 图?——避免“一张大表全塞”的新手陷阱
很多同学一上来就CREATE TABLE drug_all_in_one,结果发现“买药人退药”和“制药商退订”混在一起,库存更新逻辑彻底乱套。这份报告的硬核之处在于:它用3 张分 E-R 图(药品存储、药品售出、药品退订)强行切开业务域,再合并成全局 E-R 图。你看它的实体定义:
Drug(药品)只管自身属性(编号、名称、进价、售价);Patient(买药人)和Maker(制药商)完全解耦,各自独立主键;DBuy(药品售出)和BBack(药品退回)是两个独立关系表,而非DealType字段的取值枚举——这直接决定了后续触发器能否精准响应不同事件。
这种切割不是为了画图好看,而是为后续UPDATE Stored.Quantity触发逻辑提供原子性保障:售出减库存、退回加库存、订购加库存、退订减库存,四条路径互不干扰。如果你跳过这步直接建表,后期改结构的成本是重写全部触发器。
2.2 9 张表的字段设计暗藏业务铁律:从check(price1>0)到check(Supply='订购')
附录 1 的建表语句不是随意写的。我们逐表拆解关键约束及其业务含义:
| 表名 | 关键字段 | 约束写法 | 业务意义 |
|---|---|---|---|
Drug | price1,price2 | check(price1>0), check(price2>0) | 进价售价绝不能为负或零,堵死录入错误源头 |
Patient | Page,Psex | check(Page>=1 and Page<=120), check(Psex='男' or Psex='女') | 年龄范围强制校验,性别限定中文枚举,避免NULL或乱码导致统计失效 |
DOrder | Supply | check(Supply='订购') | 注意!这里只允许‘订购’—— 因为退订由另一张表OBack承担,用表分离代替字段枚举,确保事务隔离 |
Stored | (Dno,Lno) | primary key(Lno,Dno) | 库存主键是“柜台+药品”组合,意味着同一药品可在不同柜台存放,支持多货架管理 |
提示:
DOrder表中check(Supply='订购')是刻意为之的设计。若写成check(Supply in ('订购','退订')),则无法与OBack表解耦,后续触发器会因同一张表处理双向操作而逻辑混乱。这是用结构换清晰的典型 trade-off。
2.3 外键不是摆设:4 层级级联关系如何防止“孤儿数据”
所有业务表都通过外键锚定核心实体,形成强一致性链条:
-- 药品售出表 DBuy 必须关联真实药品和真实买药人 create table DBuy( Pno char(5) not null, Dno char(5) not null, -- ...其他字段 foreign key(Pno) references Patient(Pno), -- 买药人不存在?禁止插入! foreign key(Dno) references Drug(Dno), -- 药品不存在?禁止插入! primary key(Pno,Dno) );这种设计直接杜绝了“卖了一盒不存在的药”或“给不存在的人记销售账”的脏数据。实测时若删除Drug表中某药品,所有关联的DBuy、BBack、Stored记录会因外键约束失败而无法删除——这恰恰是保护数据完整性的正确行为。你需要的不是“删得爽”,而是“删得安全”。
2.4 主键策略暴露真实业务场景:复合主键 vs 单列自增
Stored表用(Lno,Dno)作主键,而非添加id int identity(1,1):
- ✅ 正确:一个柜台只能存一种药品一次(避免重复记录),且天然支持“查某柜台所有药品”或“查某药品在所有柜台分布”;
- ❌ 错误:若用自增 ID,需额外加
unique(Lno,Dno)约束,且查询时多一层 JOIN。
同理,DOrder主键为(Mno,Dno),因为“同一制药商对同一药品的订购单”具有业务唯一性。这种主键设计不是教科书理论,而是药店实际单据逻辑的映射。
3. 触发器才是灵魂:4 个 AFTER INSERT 触发器如何实现库存自动同步(附避坑指南)
3.1 库存变更的四大触发场景与对应 SQL 逻辑
系统用 4 个AFTER INSERT触发器捕获业务事件,实时更新Stored表库存量。其核心逻辑高度统一:从 inserted 临时表取新数据 → 关联 Stored 表 → 按业务规则加减 Quantity。
场景 1:药品订购(DOrder_insert)→ 库存增加
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;逻辑说明:DOrder插入新订购记录时,inserted表含Dno和Quantity,直接加到Stored对应药品的库存上。注意此处未指定Lno(柜台),意味着订购默认增加“总库存”,符合药店“先入库再分柜”的流程。
场景 2:药品退订(OBack_insert)→ 库存减少
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;参数说明:与订购触发器镜像对称,但减法操作。关键点在于:OBack表本身不存Lno,所以退订影响全局库存,符合“退给厂家,货直接出库”的业务。
场景 3:药品售出(DBuy_insert)→ 库存减少
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;逻辑说明:顾客买药,库存实时扣减。此处同样未限定柜台,因售出动作面向“药品总量”,而非特定柜台(实际取药时才定位柜台)。
场景 4:药品退回(BBack_insert)→ 库存增加
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;参数说明:顾客退药,库存回填。四个触发器共用同一套UPDATE...FROM语法,确保逻辑一致性。
3.2 避坑:触发器常见翻车现场与血泪修复方案
现象 → 原因 → 解决,全是实测踩过的坑:
现象:插入
DOrder后,Stored.Quantity没变化,甚至变NULL
原因:Stored表中无对应Dno的初始记录(即该药品尚未入库),UPDATE语句找不到匹配行,Quantity保持NULL或原值。
解决:在DOrder_insert触发器中加入INSERT分支,若Dno不存在则初始化库存: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; -- 若未更新到任何行,则插入新库存记录(假设默认存于Lno='L001'柜台) insert into Stored (Dno, Lno, Quantity) select inserted.Dno, 'L001', inserted.Quantity from inserted where not exists ( select 1 from Stored s where s.Dno = inserted.Dno );现象:同一药品多次订购,触发器执行后库存只加最后一次的数量
原因:UPDATE...FROM语句中inserted是多行集,但WHERE Stored.Dno = inserted.Dno未考虑多行匹配,SQL Server 默认取inserted中某一行参与计算。
解决:改用JOIN显式关联,并用SUM()聚合:create trigger DOrder_insert on DOrder after insert as update s set s.Quantity = s.Quantity + t.TotalQty from Stored s inner join ( select Dno, sum(Quantity) as TotalQty from inserted group by Dno ) t on s.Dno = t.Dno;现象:触发器执行后,
Stored表出现负库存(如 Quantity = -5)
原因:售出或退订量大于当前库存,UPDATE无校验直接运算。
解决:在触发器中加入库存充足性检查,不足则ROLLBACK并报错:create trigger DBuy_insert on DBuy after insert as if exists ( select 1 from inserted i inner join Stored s on i.Dno = s.Dno where s.Quantity < i.Quantity ) begin raiserror('药品库存不足,无法完成销售!', 16, 1); rollback transaction; return; end update Stored set Stored.Quantity = Stored.Quantity - inserted.Quantity from Stored, inserted where Stored.Dno = inserted.Dno;现象:
Stored_quohuo触发器(库存不足提醒)不打印消息,或消息重复刷屏
原因:print语句在触发器中不阻塞执行,且对多行inserted会循环执行;SELECT语句在 SSMS 中可能被忽略。
解决:改用RAISERROR抛出警告(SQL Server 2005+),或写入日志表:create trigger Stored_quohuo on Stored after insert,update as insert into LogTable (LogTime, Message) select getdate(), '药品货不足:Dno=' + Dno + ', Quantity=' + cast(Quantity as varchar) from inserted where Quantity < 10;
4. 视图与权限:如何用 4 个 WITH CHECK OPTION 视图实现“买药人只能查药,管理员能管一切”
4.1 视图不是简单 SELECT:WITH CHECK OPTION是数据写入的安全阀
报告中创建的 4 个视图(DM_P,DM_M,PD_M,DS_M)全部带WITH CHECK OPTION,这是关键!它确保:通过视图插入/更新的数据,必须满足视图定义的 WHERE 条件。以DM_P(买药人查药视图)为例:
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;逻辑说明:此视图只展示“已订购药品”的信息(通过DOrder关联)。若用户试图通过INSERT INTO DM_P添加一条记录,SQL Server 会强制检查:新数据是否能在Drug,Maker,DOrder三表连接中找到匹配行。若DOrder中无对应订购记录,则插入失败——这天然阻止了“添加未采购药品”的越权操作。
4.2 四类视图的权限边界与业务映射
| 视图名 | 查询主体 | 可见字段 | 核心限制条件 | 业务价值 |
|---|---|---|---|---|
DM_P | 买药人 | 药品名、规格、品牌、厂家信息 | 必须存在于DOrder(已采购) | 让顾客查到“店里真有”的药,不显示未进货的 SKU |
DM_M | 管理员 | 药品全字段+厂家信息 | 必须存在于OBack(有退订记录) | 聚焦“问题药品”,便于追溯质量纠纷 |
PD_M | 管理员 | 买药人信息+购买明细 | 必须存在于BBack(有退药记录) | 快速定位常退药顾客,分析退药原因 |
DS_M | 管理员 | 药品+柜台+库存量 | 必须存在于Stored(有库存记录) | 实时掌握各柜台货位,指导补货 |
注意:
DS_M视图定义中存在笔误(Drug.Dno=Stored.Lno应为Drug.Dno=Stored.Dno),实际使用前需修正。这是文档原始缺陷,但恰好说明——视图必须经SELECT * FROM 视图名实测验证,不能只信文档。
4.3 如何用视图+用户角色实现最小权限原则
仅建视图不够,需配合用户授权:
-- 创建只读角色 CREATE ROLE role_buyer; GRANT SELECT ON DM_P TO role_buyer; CREATE ROLE role_admin; GRANT SELECT, INSERT, UPDATE, DELETE ON Drug TO role_admin; GRANT SELECT, INSERT, UPDATE, DELETE ON Stored TO role_admin; GRANT SELECT ON DM_M, PD_M, DS_M TO role_admin; -- 创建用户并分配角色 CREATE LOGIN buyer_user WITH PASSWORD = '123'; CREATE USER buyer_user FOR LOGIN buyer_user; EXEC sp_addrolemember 'role_buyer', 'buyer_user'; CREATE LOGIN admin_user WITH PASSWORD = '456'; CREATE USER admin_user FOR LOGIN admin_user; EXEC sp_addrolemember 'role_admin', 'admin_user';效果:buyer_user登录后,SELECT * FROM Drug报错(无权限),但SELECT * FROM DM_P可查;admin_user可操作核心表,但普通员工无法绕过视图直接改库存。这才是企业级权限设计的雏形。
5. 存储过程封装:6 个CREATE PROCEDURE如何让增删改查变成“一句话命令”
5.1 为什么不用裸 SQL?——存储过程解决三大痛点
- 参数化防注入:
@drug_no char(5)明确类型长度,比拼接字符串安全; - 逻辑复用:
Drug_insert过程被多处调用,修改进价校验只需改一处; - 事务控制:后续可扩展为
BEGIN TRY...BEGIN CATCH包裹,保证多表操作原子性。
5.2 6 个过程的功能矩阵与调用示例
| 过程名 | 功能 | 典型调用 |
|---|---|---|
Drug_insert | 新增药品 | EXEC Drug_insert 'D0001', '阿司匹林', '解热镇痛', '100片/瓶', '拜耳', 8.5, 15.0 |
Patient_insert | 新增顾客 | EXEC Patient_insert 'P0001', '张三', 35, '男', '13800138000', '北京市朝阳区' |
Maker_insert | 新增厂家 | EXEC Maker_insert 'M0001', '拜耳医药', '德国', '4008101166' |
Storage_insert | 新增柜台 | EXEC Storage_insert 'L0001', '西药柜台A' |
DBuy_Time_select | 按时间查销售 | EXEC DBuy_Time_select '2023-01-01' |
BBack_Time_select | 按时间查退货 | EXEC BBack_Time_select '2023-01-01' |
关键细节:DBuy_Time_select过程中SELECT Pno,Dno,Quantity FROM DBuy WHERE Time_SD=@dbt_time,但Time_SD是datetime类型,若传入'2023-01-01'(无时分秒),将只匹配00:00:00的记录。正确做法是传入范围:
-- 修改过程,接收起止时间 ALTER PROCEDURE DBuy_Time_select @start_time datetime, @end_time datetime AS SELECT Pno, Dno, Quantity FROM DBuy WHERE Time_SD BETWEEN @start_time AND @end_time; -- 调用:EXEC DBuy_Time_select '2023-01-01 00:00:00', '2023-01-01 23:59:59'5.3 存储过程与触发器的协同:订单插入 → 自动建库存 → 触发器更新
这是整个系统的黄金链路:
- 用户执行
EXEC Drug_insert 'D0001', ...新增药品; - 执行
EXEC DOrder_insert 'M0001', 'D0001', 100, GETDATE(), '订购'下采购单; DOrder_insert触发器自动执行UPDATE Stored SET Quantity += 100;- 若
Stored中无D0001记录,按 3.2 节修复方案自动INSERT初始化。
验证方法:在 SSMS 中依次执行上述步骤,然后SELECT * FROM Stored WHERE Dno='D0001',确认Quantity为 100。若为 0 或 NULL,说明触发器未生效或Stored表缺失初始记录——此时回溯检查DOrder_insert触发器逻辑及Stored表是否存在。
6. 从 SQL Server 2000 到现代环境:5 个必改项与 1 个终极验证技巧(附迁移 checklist)
6.1 5 个 SQL Server 2000 → 2012+/2019/2022 的必改项
原始脚本为 SQL Server 2000 编写,直接在新版运行会报错。以下是实测有效的最小改动清单:
| 原始写法 | 新版推荐写法 | 原因 |
|---|---|---|
create table user(ID number(4) primary key, Postword number(6) not null) | create table [user](ID char(4) primary key, Password char(6) not null) | user是保留关键字,必须加[ ];number类型已废弃,改用char;字段名Postword应为Password(文档笔误) |
create unique index DLno on Stored(Dno,Lno) | CREATE UNIQUE NONCLUSTERED INDEX IX_Stored_Dno_Lno ON Stored(Dno,Lno) | 新版语法要求显式声明索引类型(NONCLUSTERED)和名称(IX_前缀) |
print('药品货不足') | RAISERROR('药品货不足', 10, 1) | PRINT在触发器中不阻塞,RAISERROR可控严重级别(10=信息,16=错误) |
check(Psex='男' or Psex='女') | check(Psex in ('男','女')) | 语义更清晰,兼容性更好 |
create datebase DrugStore | CREATE DATABASE DrugStore | 关键字DATABASE拼写修正 |
6.2 终极验证技巧:用“三查一跑”法 10 分钟确认系统可用性
不要一上来就写复杂查询,按顺序执行以下四步,任一步失败即停:
查表结构:
SELECT TABLE_NAME FROM DrugStore.INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE'
→ 应返回 9 张表(user,Drug,Patient,Maker,Storage,DOrder,OBack,DBuy,BBack,Stored)。缺表?检查附录 1 的CREATE TABLE语句是否全部执行。查触发器状态:
SELECT name, is_disabled FROM sys.triggers WHERE parent_id = OBJECT_ID('Stored')
→ 所有触发器is_disabled应为 0。若为 1,执行ENABLE TRIGGER trigger_name ON Stored。查视图数据:
SELECT TOP 1 * FROM DM_P
→ 若报错“对象名 'DM_P' 无效”,说明视图创建失败(常见于DOrder表名写错为Order_Back);若返回空,说明DOrder中无数据,需先插入测试订购记录。跑核心业务流(10 分钟闭环验证):
-- Step 1: 插入测试药品 INSERT INTO Drug VALUES ('D0001', '阿司匹林', '解热镇痛', '100片/瓶', '拜耳', 8.5, 15.0); -- Step 2: 插入测试厂家 INSERT INTO Maker VALUES ('M0001', '拜耳医药', '德国', '4008101166'); -- Step 3: 下采购单(触发库存增加) INSERT INTO DOrder VALUES ('M0001', 'D0001', 100, GETDATE(), '订购'); -- Step 4: 查库存是否为100 SELECT Dno, Quantity FROM Stored WHERE Dno = 'D0001'; -- 应返回 D0001, 100 -- Step 5: 模拟销售(触发库存减少) INSERT INTO DBuy VALUES ('P0001', 'D0001', GETDATE(), 5, '售出'); -- Step 6: 查库存是否为95 SELECT Dno, Quantity FROM Stored WHERE Dno = 'D0001'; -- 应返回 D0001, 95成败标志:Step 4 和 Step 6 的
Quantity值必须精确匹配预期。若不符,立即检查DOrder_insert和DBuy_insert触发器内容——90% 的问题出在这里。
从那以后我每次迁移老数据库脚本,都强制走一遍“三查一跑”:不看文档描述,只信这四步的输出。文档可能写错字段名,但SELECT * FROM sys.triggers不会骗人;老师可能说“触发器已调试好”,但SELECT * FROM Stored的数字不会撒谎。这份药品管理系统报告的价值,不在它多完美,而在它足够真实——真实到每个CHECK约束都在防一个具体错误,每个触发器都在解一个具体业务痛点,每个视图都在划一条具体的权限边界。它不教你“数据库是什么”,它逼你亲手把“药品购进”这个动作,变成一行INSERT和一次UPDATE。希望帮到你。
本文还有配套的精品资源,点击获取