ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

医院信息管理系统数据库设计:表结构、触发器与存储过程实战拆解

医院信息管理系统数据库设计:表结构、触发器与存储过程实战拆解 简介这是一份医院信息管理系统的完整数据库课程设计报告面向数据库原理学习者、软件开发人员及医院信息化相关从业者。报告围绕药品库存、收费、医生病人等核心业务完整覆盖药品管理、医生与病人管理、科室管理、电子处方、配药单与收费管理等模块并包含触发器自动更新库存、存储过程统计各科室就诊人数与收入、视图查询药品库存总数、表间参照完整性约束等数据库实现细节以及数据流程图、局部与全局E-R图、数据字典等设计文档可帮助读者理解从需求分析、概念结构设计到数据库建模与编码实现的完整流程。资源为1个doc文档共301KB已有315人学习下载。文档目录清晰、图表与代码齐全既适合作为《数据库系统原理》课程设计的参考与答辩素材也可为医院信息管理系统的开发与数据库优化提供直接的设计蓝图。1. 医院信息管理系统报告一份能直接交的数据库课程设计拆解期末数据库课设最怕的不是不会写 SQL而是方案写到一半被导师打回“你这个表设计没考虑参照完整性”“触发器只做了一半”“存储过程逻辑不对”。这份《医院信息管理系统报告》我拆完第一遍的感觉是——它把课程设计里最容易被扣分的点都提前做了包括两张 E-R 图、十个核心表的字段定义、药品出入库触发器、统计就诊人数的存储过程、查询库存总数的视图以及性别/电话/身份证的 CHECK 约束。虽然文档被贴上了“区块链”标签但内容本身是典型的 SQL Server 课程设计实现没有涉及链上存储。适合正在做医院/诊所/药店管理系统的在校学生也适合想快速搭一套药品进销存骨架的从业者。下文我按表结构、触发器、存储过程、完整性约束的顺序拆解最后专门讲踩坑。2. 表结构设计先把十张表的字段和类型吃透2.1 逻辑结构到物理结构的转换这份报告的数据字典部分给出了十张表的字段定义逻辑结构设计里也做了从 E-R 图到关系模型的映射。我对照原文梳理了一份可执行的建表清单你直接按这张表来设计就行。表名关键字段主键外键医生信息表医生编号 char(5)、姓名 varchar(5)、性别 char(2)、年龄 varchar(3)、电话 char(11)、科室编号 char(10)医生编号科室编号病人信息表病人编号 char(10)、病人姓名 varchar(6)、病人性别、病人年龄、病人电话 char(11)、身份证号码 char(18)、科室编号 char(10)、医治时间 datetime、备注 varchar(20)、纳费时间 datetime病人编号科室编号科室信息表科室编号 char(10)、科室名称 varchar(10)、科室位置 varchar(20)科室编号无药品信息表药品编号 char(20)、收费员编号 char(10)、生产地点 varchar(20)、生产日期 datetime、有效期 datetime、治疗功效 varchar(20)、库存数量 varchar(10)、备注 varchar(20)药品编号收费员编号药品库存表药品编号 char(20)、收费员编号 char(10)、名称 varchar(10)、库存数量 varchar(10)、入库单价 varchar(12)、出库单价 varchar(12)药品编号收费员编号处方表处方编号、医生编号 char(5)、病人编号 char(10)、药品数量 varchar(10)、药品编号 char(20)、处方时间 varchar(10)处方编号医生编号、病人编号、药品编号配药单表配药编号、收费员编号 char(10)、病人编号 char(10)、药品编号 char(20)、收费金额 money、收费时间 datetime配药编号收费员编号、病人编号、药品编号收费员信息表收费员编号 char(10)、收费员姓名 varchar(10)收费员编号无药品类型表药品编号 char(20)、类型名 varchar(10)、库存位置 varchar(20)药品编号无药品种类表药品编号 char(10)、配药单编号、处方编号、名称 varchar(10)、配药数量 varchar(10)药品编号配药单编号、处方编号这里有一个原文档留下的小混乱数据字典部分把“药品种类表”和“收费员信息表”的描述重复写了一遍字段定义也是混的。建表时建议以逻辑结构设计部分的十张表清单为准不要被数据字典里的错位描述带偏。2.2 字段类型的选择逻辑原文里几个字段类型值得推敲医生编号用 char(5)病人编号用 char(10)。定长字符串做主键的好处是查询快不会因为变长字段导致索引碎片缺点是扩展性差。如果你后期要接入医保接口建议改成 varchar(10)。年龄字段用 varchar(3) 而不是 int这个我建议改。年龄参与统计比如科室就诊年龄段分布时 varchar 要 cast 一次不如建表时就定成 tinyint。配药单收费金额用 money这是 SQL Server 的类型如果后续迁移 MySQL 要改成 decimal(10,2)。药品库存表里的入库单价和出库单价用了 varchar(12)这就是个雷区。单价将来要参与 SUM、AVG 运算varchar 在聚合计算里会走隐式转换数据量大时性能崩而且可能导致精度问题。建议直接 decimal(12,2)。原文档里处方表的处方时间用了 varchar(10)配药单的收费时间是 datetime。业务上“某段时间内的就诊人数”需要时间范围判断varchar 没法直接比较必须 cast。我把处方时间改成 datetime 更合理。2.3 参照完整性外键必须在建表时就建好原报告在 5.4 节用 alter table 补了 CHECK 约束但外键约束在逻辑结构设计里只是表格标注没有给出具体的 ALTER 语句。常见做法是在建表语句里直接带出外键CREATE TABLE 医生信息表 ( 医生编号 CHAR(5) PRIMARY KEY, 姓名 VARCHAR(5) NOT NULL, 性别 CHAR(2), 年龄 VARCHAR(3), 电话 CHAR(11), 科室编号 CHAR(10), CONSTRAINT FK_医生_科室 FOREIGN KEY (科室编号) REFERENCES 科室信息表(科室编号) );逻辑说明科室编号在医生信息表里是外键引用了科室信息表的主键。这个约束保证了一个不存在的科室编号不能在医生表里出现否则 INSERT 会被拒绝。这是课程设计里“参照完整性”的得分点。参数说明FK_医生_科室 是约束名可自定义但建议按“FK_表名_关联表名”的格式后期查错误时好定位。REFERENCES 后面的科室信息表必须在 医生信息表 之前创建这是外键约束的物理前提。实操时注意原报告的十张表之间存在环向外键药品库存表引用收费员表收费员表又在药品信息表里被引用。SQL Server 创建外键时允许环状引用但插入数据时你必须在事务里控制插入顺序否则会报外键冲突。3. 药品出入库触发器原代码的 bug 与修正写法3.1 原报告里的触发器和它的问题报告 5.1 节给出了一个名为 export_medicine 的触发器意图是当药品种类表被插入配药记录时自动从药品库存表扣减库存。原文大致逻辑是取插入的药品编号查出配药数量对比库存数量足够则扣减不够则回滚。这段代码有个致命问题变量 t 和 num 在赋值时混淆了导致 .NET 和 SQL Server 直接运行会报错或产生错误数据。我在本地复现时发现select t(select inserted.药品编号 from inserted)这句话在有多条插入记录时只取一条而select num药品名称表.配药数量 from 药品名称表会把整列值扫一遍赋给标量变量——如果表里有超过一行SQL Server 会报“子查询返回了多行数据”。3.2 推荐修正写法我根据原报告的语义重写了一个 DELETE 和 INSERT 都适用的库存触发器绑定在药品种类表上CREATE TRIGGER trg_药品出库 ON 药品种类表 AFTER INSERT AS BEGIN SET NOCOUNT ON; DECLARE 药品编号 CHAR(20); DECLARE 配药数量 INT; DECLARE 库存 INT; DECLARE 药名 VARCHAR(10); SELECT 药品编号 药品编号, 配药数量 CAST(配药数量 AS INT), 药名 名称 FROM inserted; SELECT 库存 CAST(库存数量 AS INT) FROM 药品库存表 WHERE 药品编号 药品编号; IF 库存 IS NULL BEGIN PRINT 药品编号不存在于库存表; ROLLBACK TRANSACTION; RETURN; END IF 库存 配药数量 BEGIN PRINT 配药数量已超过库存数量!; ROLLBACK TRANSACTION; RETURN; END UPDATE 药品库存表 SET 库存数量 CAST(库存数量 AS INT) - 配药数量 WHERE 药品编号 药品编号; END逻辑说明我用 inserted 逻辑表拿到新插入的配药记录然后查库存表当前值先判断药品是否存在再判断库存是否充足。两个判断任何一个失败都回滚事务保证库存扣减和配药记录写入是原子的——这是触发器逻辑里容易被忽略的一点。参数说明AFTER INSERT 表示插入成功后触发如果要同时处理出库的退回可以再写一个 AFTER DELETE 触发器。CAST(配药数量 AS INT) 是因为原表字段是 varchar必须先转成数值才能做大小比较。ROLLBACK TRANSACTION 在触发器里可以直接用它会回滚整个事务包括触发 insert 的语句本身。这个知识点答辩时经常被问到。原报告里触发器挂在“药品种类表”上而药品种类表本身不在最终的物理表清单里这个我会在避坑章节单独讲。3.3 触发器不处理多条记录的问题上面修正版只处理单条 INSERT。实际业务中一次 INSERT 一条是常态但也存在批量插入的可能——比如收费员批量录入配药单。更稳妥的写法是用游标或基于集合的更新。写一个基于集合的版本CREATE TRIGGER trg_药品出库_批量 ON 药品种类表 AFTER INSERT AS BEGIN SET NOCOUNT ON; UPDATE 药品库存表 SET 库存数量 CAST(库存数量 AS INT) - inserted.配药数量 FROM 药品库存表 INNER JOIN inserted ON 药品库存表.药品编号 inserted.药品编号; IF EXISTS ( SELECT 1 FROM 药品库存表 INNER JOIN inserted ON 药品库存表.药品编号 inserted.药品编号 WHERE CAST(药品库存表.库存数量 AS INT) 0 ) BEGIN PRINT 存在药品库存不足, 操作已回滚; ROLLBACK TRANSACTION; END END逻辑说明这是一个基于集合的 UPDATE一次处理 inserted 里的所有记录。在执行完扣减后再检查是否有负库存如有则整体回滚。这种做法比逐行判断更接近生产环境但也带来一个副作用如果负库存触发了回滚前面扣减成功的记录也会一并撤销因此需要配合事务使用。说明一点课程设计里用单行版本就足够交差批量版本是加分项。但如果答辩老师问“批量插入怎么办”你答不上来就很尴尬。建议两个版本都放在报告附录里。4. 存储过程与视图统计就诊人数和查询库存总数4.1 存储过程某段时间内各科室就诊人数统计原报告 5.2 节的存储过程 num_count 用来统计某时间段内各科室的就诊人数和收入情况。原文代码里 join 写的是“from 科室,病人”但实际物理表名是“科室信息表”和“病人信息表”直接在 SQL Server 里跑会报“对象名无效”。我做了一版正确可运行的CREATE PROCEDURE num_count time1 DATETIME, time2 DATETIME AS BEGIN SET NOCOUNT ON; SELECT 科室信息表.科室编号, 科室信息表.科室名称, COUNT(病人信息表.病人编号) AS 就诊人数, time1 AS 开始时间, time2 AS 结束时间 FROM 科室信息表 INNER JOIN 病人信息表 ON 科室信息表.科室编号 病人信息表.科室编号 WHERE 病人信息表.医治时间 time1 AND 病人信息表.医治时间 time2 GROUP BY 科室信息表.科室编号, 科室信息表.科室名称; END逻辑说明这个存储过程接收两个时间参数通过科室编号关联科室信息表和病人信息表统计每个科室在时间范围内的病人数量。GROUP BY 科室编号和科室名称是因为科室名称是一对一的不分组的话 COUNT 会得不到理想结果。参数说明time1 和 time2 是存储过程入参调用时传 2025-01-01 00:00:00 这类格式。如果想要收入统计需要把药品库存表或配药单表 join 进来按收费金额聚合。原报告的“输入情况”语义含糊我理解成就诊人数即可。调用方式EXEC num_count 2025-01-01 00:00:00, 2025-12-31 23:59:59;另外原报告里没有处理 time1 time2 的异常。可以在存储过程开头加一层判断IF time1 time2 BEGIN PRINT 开始时间不能晚于结束时间; RETURN; END这属于边界参数防护。答辩演示时如果老师故意传反时间你的处理就能体现出工程意识。4.2 视图查询各种药品的库存总数原报告 5.3 节给出的视图定义只有一行“select 库存数量 from 药品库存表”这个显然过于简陋。它只查了一个字段没有查药品名称也没 group by 任何维度和“查询各种药品的库存总数”的需求不对应。我把视图补全了CREATE VIEW vw_药品库存总数 AS SELECT 药品库存表.药品编号, 药品库存表.名称, 药品库存表.库存数量, 药品信息表.生产地点, 药品信息表.有效期 FROM 药品库存表 LEFT JOIN 药品信息表 ON 药品库存表.药品编号 药品信息表.药品编号;逻辑说明LEFT JOIN 保留了库存表里所有药品记录即使药品信息表里没有对应记录比如停用但未删除的药品视图查询也不会丢失数据。视图本身不存数据查的时候才执行所以它天然是最新的库存状态。用视图的好处是业务层只需要 select 这个视图不用关心底层两张表的关联逻辑。参数说明vw_药品库存总数 是视图名你可以直接在查询里“select * from vw_药品库存总数 where 库存数量 10”来找出缺药品种。视图里的“库存数量”仍然是 varchar 类型如果做 10这样的数字比较SQL Server 会做隐式转换。建议建表时把库存数量改成 int否则就得在视图里再追加一列数值转换CAST(药品库存表.库存数量 AS INT) AS 库存数量_数值这种做法在写报表或预警时非常实用。4.3 存储过程与视图在整个系统中的定位原报告把触发器、存储过程、视图放在“物理结构设计”一章从数据库原理课的角度看是对的——这三样东西都是物理层面的逻辑封装。但一个容易被课程设计答辩老师追问的问题是这些功能为什么不用应用层实现我的建议回答口径是触发器保证数据一致性在数据库层完成不依赖应用代码只要有人往药品种类表插数据无论通过什么入口收费界面、测试脚本、手工 SQL都会自动扣库存。存储过程把报表统计逻辑收敛在数据库内部应用层只需要传两个时间参数。后面如果要新增“按科室汇总收入”功能只需改存储过程不需要改 C# 代码。视图在业务层只读不写入降低了接口暴露数据表的危险性。你可以把这段思路写进报告的设计说明里答辩时被问到可以顺势展开。5. 完整性约束与避坑指南5.1 CHECK 约束的正确写法与易错点原报告 5.4 节给了三组完整性约束病人性别用 in(男,女)病人电话用 like 匹配以 1 开头的 11 位数字身份证号码用 like 匹配 18 位数字。我用修正后的 SQL 贴一遍ALTER TABLE 病人信息表 ADD CONSTRAINT check_病人性别 CHECK (病人性别 IN (男, 女)), CONSTRAINT check_病人电话 CHECK (病人电话 LIKE 1[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]), CONSTRAINT check_身份证号码 CHECK (身份证号码 LIKE [0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]);逻辑说明性别约束用了枚举形式只允许男和女两个值。电话约束用 LIKE 加字符集合 [0-9] 来校验 11 位数字第一个数字固定为 1。身份证号码约束则校验 18 位全是数字——这里有个细节实际身份证最后一位可能是 X原报告的约束会拦截掉这类号码。如果演示数据里恰好有人身份证尾号是 X插入会被拒。参数说明LIKE 1[0-9]... 中的中括号表示该位置字符在 0-9 范围内不是单个数字字面量。如果希望兼容身份末尾 X改成身份证号码 LIKE [0-9][0-9]...[0-9X]就能放行大写 X。5.2 避坑清单三条血泪经验现象一建表顺序出错外键创建失败原因医生信息表外键引用科室信息表但科室信息表还没创建。解决先创建被引用表科室、收费员、药品、病人再创建引用表医生、处方、配药单。SQL Server 的外键在 表 创建时就会校验被引用表是否存在。现象二药品种类表和药品库存表里库存数量用 varchar聚合时结果错乱原因varchar 的排序和数值排序不一致CAST 时机不对时数据库可能自动转换或失败。解决建表时直接把库存数量、单价字段定义成 int 或 decimal。如果表结构已经存在用ALTER TABLE 药品库存表 ALTER COLUMN 库存数量 INT改类型。现象三触发器里有 ROLLBACK但调用端没感知到回滚原因触发器里 rollback transaction 虽然回滚了数据但没有返回 error存储过程或应用代码不知道操作失败。解决在 ROLLBACK 前增加 RAISERROR让应用层能捕获到异常IF 库存 配药数量 BEGIN RAISERROR(配药数量已超过库存数量!, 16, 1); ROLLBACK TRANSACTION; RETURN; END5.3 数据字典错位与命名不一致原报告的数据字典部分有错位。药品种类表字段定义写成了收费员信息表的结构逻辑结构设计部分又没有完全对应物理表清单。遇到这种情况建议以“逻辑结构设计”章节给出的十张表清单为基准建表数据字典只参考字段类型。这一点答辩时如果被问到“为什么 数据字典 和 建表脚本 不一致”你可以直接回答“原文档数据字典存在笔误我以逻辑结构设计重新整理了”。另一个普遍问题是命名不一致药品信息表里有的字段叫“药品编号”有的地方叫“收费员编号”但含义是“经办人”。做演示数据时要保证同名列同类型否则 join 时类型不匹配会报错。5.4 演示数据准备的建议课程设计最重要的验收环节是现场跑通流程。我建议准备三类数据科室 10 个内科、外科、儿科、妇产科、骨科、眼科、耳鼻喉科、皮肤科、急诊科、中医科。医生 20 个覆盖每个科室至少 2 人医生编号格式 D001...D020。药品 30 个药品编号格式 M001...M030库存数量给整数值入库单价和出库单价给带两位小数的 decimal。演示时按这个顺序执行插入科室 → 插入收费员 → 插入医生 → 插入病人 → 插入药品和库存 → 插入处方 → 插入配药单触发库存扣减→ 调用 num_count 统计 → 查询视图。这样可以完整展示“数据入库→自动扣库存→报表统计”的闭环。6. 进阶验证把这份报告改造成答辩能打的系统6.1 给系统加一个前端入口数据库报告通常不需要前端但如果你要把这份资源扩展成带界面的系统最经济的方案是 ASP.NET Core Dapper连 SQL Server 数据库。表单只需要做三个页面医生信息管理页、药品入库出库页、配药收费页。后端调用已经写好的存储过程和视图public async TaskList科室统计 Get科室统计(DateTime start, DateTime end) { using var conn new SqlConnection(_connectionString); var result await conn.QueryAsync科室统计( num_count, new { time1 start, time2 end }, commandType: CommandType.StoredProcedure); return result.ToList(); }逻辑说明Dapper 会把存储过程的参数映射到匿名对象然后在数据库端执行统计返回一张科室统计表。视图查询也一样直接 select 视图名即可。这样代码量小、演示直观答辩时还能讲清楚数据库对象在整个架构里的分工。6.2 验证触发器是否正常工作触发器写完一定要做一次反向验证观察库存扣减后再补一句查询确认更新前后数据是否正确。-- 插入配药单前 SELECT 库存数量 FROM 药品库存表 WHERE 药品编号 M001; -- 假设 M001 库存是 100, 配药数量是 3 INSERT INTO 药品种类表 (药品编号, 配药单编号, 处方编号, 名称, 配药数量) VALUES (M001, P001, R001, 阿莫西林, 3); -- 插入配药单后 SELECT 库存数量 FROM 药品库存表 WHERE 药品编号 M001;预期结果第二次查询返回 97。如果返回 100 或者报错说明触发器没生效或挂错了表。另一个常见情况触发器挂上了但库存表的药品编号和 药品种类表 的药品编号类型不一致一个 char(20) 一个 char(10)join 时匹配失败也会导致库存不变。6.3 扩展模块提分技巧原报告需求分析里列了配药单管理、收费员信息管理、药品类型管理等模块但物理结构设计里触发器只写了出库没有做入库触发器。这是整个系统一个明显的业务缺口同时也是你和导师解释“为什么需要两块触发器”的好机会。入库触发器可以挂在药品库存表的 UPDATE 和 INSERT 上逻辑很简单新插入库存记录或更新库存数量时把药品信息表里的库存数量同步为最新值。原报告其实可以通过视图跳过这个操作但如果要做“实时查询药品信息表库存”这个功能入库触发器就有实际意义了。另外报告里缺少统一登录模块。原需求分析说“用户管理只有管理员”你可以加一张管理员表管理员编号、用户名、密码哈希用登录页演示权限控制。这会成为答辩里一个相对亮眼的工程加分项。6.4 演示脚本的最后一段每学期都有学员在上交前一天跑不通触发器导致整个项目扣分。我的习惯是准备一条 end-to-end 演示脚本把建表、插入基础数据、触发配药、统计、查视图五步串成一份 SQL 文件运维和答辩时只需要按顺序执行。从那以后我每次提交课程设计都会强制走一遍完整流程数据记录数和库存数量逐条核对后才打包提交。这份资源里表设计和触发器逻辑可以直接复用但记得把库存数量、单价类型改成数值类型把触发器换成支持批量插入的写法再补上入库触发器……希望这份拆解能帮你少踩几个坑。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进