☰
医院病房管理系统数据库课设实战:ER建模、存储过程与避坑指南
2026/11/4 7:30:08 网站建设 项目流程

简介:本资源是面向高校数据库课程设计的《医院病房管理系统》完整实现项目,适用于计算机、软件工程等专业本科生开展数据库建模、B/S架构开发与医疗业务系统实践。项目覆盖病人入院登记、病房分配、医护排班、病历管理及费用结算等核心业务流程,深度融合关系数据库理论(ER建模、主外键约束、索引优化)与Java后端开发(146个文件中含37个Java源码、59个编译class)、前端界面(24个jpg+6个png资源)及SQL脚本(1个sql文件),并包含项目配置(xml、iml、classpath等)与运行环境说明(exe、log、doc文档)。压缩包大小为5.04MB,结构清晰,模块化程度高,便于分层学习与调试。目前已有167人下载学习,读者可直接导入IDE运行,获取从需求分析、数据库设计、Java编码到界面集成的全流程参考,尤其适合巩固SQL操作、理解事务处理与权限控制等课设关键能力。

1. 医院病房管理系统——不是Demo,是能跑通挂号、床位、医嘱、费用全链路的数据库课设实战包

你手头那份“数据库课设”压缩包,十有八九还躺在下载目录里吃灰:建了三张表就卡在触发器写不对,外键约束一加就报错,存储过程调用时参数死活传不进去,更别说把护士站排班、病人入出转、药品计费这些真实业务逻辑串成闭环。这份《医院病房管理系统--数据库课设.zip》不是PPT式概念演示,而是一套经真实课堂答辩验证、含完整ER图+SQL脚本+测试数据+操作手册的可执行数据库工程。它覆盖从病人挂号(含身份证校验)、科室分诊、床位分配(支持空床查询与锁定)、医嘱录入(带药品库存联动扣减)、费用结算(按项目+天数+折扣三级计算)到出院归档的全流程,所有逻辑均落地为MySQL 8.0兼容的DDL/DML/PLSQL语句。适合数据库原理刚学完、正卡在“建库容易,建业务难”阶段的本科生;也适合作为课程设计快速启动模板——你不需要重造轮子,只需要看清每条SQL背后为什么这么写、哪里容易翻车。


2. 从ER图到物理表:为什么这版设计能避开90%的外键灾难

2.1 ER模型如何映射出6张核心表及其业务约束

该系统采用清晰的“实体-关系-属性”三层建模,最终落地为6张物理表:patient(病人主档)、ward(病房信息)、bed(床位明细)、doctor(医生档案)、medical_order(医嘱主表)、order_item(医嘱明细)。关键设计决策在于弱实体强化与关系表升格:

  • bed表不作为ward的简单属性,而是独立实体,含bed_id(主键)、ward_id(外键)、bed_status(空/占用/维修)、bed_type(普通/监护/隔离)字段。此举让“查询某病区所有空床”变成单表扫描,而非关联ward后过滤。
  • medical_order与patient之间不是直接外键,而是通过order_header(已合并进medical_order)承载patient_id+admit_date+discharge_date,避免因病人未出院导致医嘱无法关联。
  • 所有外键均显式声明ON DELETE RESTRICT(禁止级联删),防止误删科室导致整张doctor表失效——这是某高校课设答辩中被老师当场揪出的高频血泪问题。

2.2 建库脚本逐行解析:从字符集到索引策略

解压后根目录下create_database.sql是唯一入口文件,需按顺序执行。重点参数说明如下:

-- 创建数据库(强制指定字符集,避坑点1) CREATE DATABASE hospital_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 切换数据库并设置时区(避坑点2:不设时区会导致NOW()返回系统默认,非东八区) USE hospital_db; SET time_zone = '+08:00'; -- patient表创建(注意:身份证号用VARCHAR(18),非INT!) CREATE TABLE patient ( patient_id CHAR(10) PRIMARY KEY COMMENT '住院号,格式:Y20240001', id_card VARCHAR(18) NOT NULL UNIQUE COMMENT '身份证号,需校验格式', name VARCHAR(20) NOT NULL, gender ENUM('男','女') NOT NULL, birth_date DATE NOT NULL, phone VARCHAR(11), admit_date DATETIME DEFAULT CURRENT_TIMESTAMP, discharge_date DATETIME NULL, ward_id CHAR(5), bed_id CHAR(8), -- 外键约束必须在字段定义后单独声明 CONSTRAINT fk_patient_ward FOREIGN KEY (ward_id) REFERENCES ward(ward_id) ON DELETE RESTRICT, CONSTRAINT fk_patient_bed FOREIGN KEY (bed_id) REFERENCES bed(bed_id) ON DELETE RESTRICT ) ENGINE=InnoDB COMMENT='病人主档表'; -- 为高频查询字段添加复合索引(避坑点3:单列索引对WHERE a=? AND b=?无效) CREATE INDEX idx_patient_ward_bed ON patient(ward_id, bed_id); CREATE INDEX idx_medical_order_patient_date ON medical_order(patient_id, order_date);

提示:脚本末尾包含INSERT INTO patient ...测试数据,共12条预置记录,含3个不同病区(内科/外科/儿科)、6种床位类型、2名主治医生。执行前请确认MySQL服务已启动且用户有CREATE权限。

2.3 字段设计背后的业务逻辑推演

为什么bed_id设计为CHAR(8)而非自增ID?因为实际医院中床位编号含语义:N0101A表示内科楼1层01房A床。若用AUTO_INCREMENT,则无法直观定位物理位置,且导出报表时需额外关联ward表反查楼层信息,徒增JOIN开销。同理,patient_id采用Y20240001格式,既保证全局唯一,又隐含年份信息,方便按年度统计住院人次——这些细节在课设报告中常被忽略,却是评审老师重点考察的“业务理解深度”。


3. 医嘱与费用联动:存储过程如何实现“开药即扣库存、结算自动累加”

3.1proc_create_medical_order:一条医嘱触发三重校验

该存储过程是系统业务中枢,接收p_patient_id,p_doctor_id,p_drug_code,p_quantity四个输入参数,执行流程如下:

  1. 病人状态校验:检查patient.discharge_date IS NULL,防止给已出院病人开新医嘱;
  2. 药品库存校验:查询drug_inventory.stock_qty >= p_quantity,库存不足则SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '药品库存不足';
  3. 床位占用校验:通过patient.bed_id关联bed.bed_status = '占用',确保病人确实在院。
DELIMITER $$ CREATE PROCEDURE proc_create_medical_order( IN p_patient_id CHAR(10), IN p_doctor_id CHAR(6), IN p_drug_code VARCHAR(20), IN p_quantity INT ) BEGIN DECLARE v_bed_status VARCHAR(10); DECLARE v_stock_qty INT DEFAULT 0; -- 步骤1:校验病人是否在院 IF NOT EXISTS (SELECT 1 FROM patient WHERE patient_id = p_patient_id AND discharge_date IS NULL) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '病人未住院或已出院,无法开具医嘱'; END IF; -- 步骤2:校验药品库存(关键:先查再扣,防超卖) SELECT stock_qty INTO v_stock_qty FROM drug_inventory WHERE drug_code = p_drug_code FOR UPDATE; -- 加行锁,避免并发扣减错误 IF v_stock_qty < p_quantity THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '药品库存不足'; END IF; -- 步骤3:插入医嘱明细(order_item) INSERT INTO order_item (order_id, drug_code, quantity, unit_price) VALUES (CONCAT('ORD', DATE_FORMAT(NOW(), '%Y%m%d'), LPAD(@row_number:=@row_number+1, 4, '0')), p_drug_code, p_quantity, (SELECT price FROM drug_catalog WHERE drug_code = p_drug_code)); -- 步骤4:扣减库存(原子操作) UPDATE drug_inventory SET stock_qty = stock_qty - p_quantity WHERE drug_code = p_drug_code; END$$ DELIMITER ;

参数说明:FOR UPDATE是核心安全机制,确保在高并发场景下不会出现“查到有10盒→A线程扣1盒→B线程也查到剩10盒→两线程各扣1盒→最终剩8盒”的超卖问题。@row_number变量用于生成订单号,格式如ORD202405200001,满足课设对“唯一性+可读性”的双重要求。

3.2proc_calculate_fee:费用结算的三级计算引擎

出院结算不简单等于“所有医嘱金额相加”,需支持:

  • 项目计费:如CT检查按次收费(固定价);
  • 日费计费:如床位费按实际住院天数×单价;
  • 折扣计费:医保病人享85%折扣,需单独标记。
DELIMITER $$ CREATE PROCEDURE proc_calculate_fee(IN p_patient_id CHAR(10)) BEGIN DECLARE v_total_fee DECIMAL(10,2) DEFAULT 0.00; DECLARE v_discount_rate DECIMAL(3,2) DEFAULT 1.00; -- 获取病人折扣率(此处简化:医保病人固定0.85,实际可查patient.insurance_type) SELECT CASE WHEN insurance_type = '医保' THEN 0.85 ELSE 1.00 END INTO v_discount_rate FROM patient WHERE patient_id = p_patient_id; -- 计算项目类费用(order_item中unit_price * quantity) SELECT COALESCE(SUM(unit_price * quantity), 0) INTO v_total_fee FROM order_item oi JOIN medical_order mo ON oi.order_id = mo.order_id WHERE mo.patient_id = p_patient_id; -- 加上日费类费用(床位费:住院天数 × 床位单价) SELECT v_total_fee + COALESCE( DATEDIFF(COALESCE(p.discharge_date, NOW()), p.admit_date) * w.daily_fee, 0 ) INTO v_total_fee FROM patient p JOIN bed b ON p.bed_id = b.bed_id JOIN ward w ON b.ward_id = w.ward_id WHERE p.patient_id = p_patient_id; -- 应用折扣 SET v_total_fee = ROUND(v_total_fee * v_discount_rate, 2); -- 写入fee_record表并更新patient状态 INSERT INTO fee_record (patient_id, total_fee, discount_rate, settle_date) VALUES (p_patient_id, v_total_fee, v_discount_rate, NOW()); UPDATE patient SET discharge_date = NOW() WHERE patient_id = p_patient_id; END$$ DELIMITER ;

关键设计:COALESCE(p.discharge_date, NOW())处理“尚未出院但需预估费用”的场景;DATEDIFF直接计算天数,比TIMESTAMPDIFF(DAY,...)更精准(后者可能因时分秒差异多算1天);所有金额运算均用DECIMAL(10,2),杜绝浮点数精度丢失——某同学曾因用FLOAT导致100.05元结算成100.049999元,被老师质疑“财务系统怎能有误差”。


4. 避坑指南:课设答辩现场被问懵的5个高频问题及根因解法

4.1 现象:执行create_database.sql报错ERROR 1005 (HY000): Can't create table 'hospital_db.patient' (errno: 150)

原因:外键引用的父表(如ward)尚未创建,或父表引擎非InnoDB(MyISAM不支持外键),或字段类型不严格匹配(如ward_id在ward表为CHAR(5),但在patient表误写为VARCHAR(5))。
解决:

  • 检查建表顺序,确保父表(ward,bed,doctor)在子表(patient,medical_order)之前创建;
  • 执行SHOW CREATE TABLE ward;确认引擎为ENGINE=InnoDB;
  • 用DESCRIBE ward;和DESCRIBE patient;对比外键字段类型、长度、是否允许NULL,必须完全一致。

4.2 现象:调用proc_create_medical_order时,库存扣减成功但医嘱未插入,或反之

原因:存储过程中未启用事务,导致部分语句成功、部分失败,数据不一致。原脚本默认自动提交,需显式包裹START TRANSACTION。
解决:在存储过程开头添加START TRANSACTION;,结尾添加COMMIT;,并在SIGNAL异常处添加ROLLBACK;:

-- 在BEGIN后立即加入 START TRANSACTION; -- ...原有逻辑... -- 在所有正常执行路径末尾 COMMIT; -- 在每个SIGNAL前加入 ROLLBACK; SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'xxx';

4.3 现象:SELECT * FROM patient WHERE ward_id = 'N01';查询极慢,执行计划显示type: ALL(全表扫描)

原因:ward_id字段未建索引,MySQL被迫扫描全部12条记录。
解决:立即执行CREATE INDEX idx_patient_ward ON patient(ward_id);。课设中常被忽略的性能点:即使数据量小(<100条),也应养成“WHERE字段必索引”习惯,否则扩展到千级数据时将彻底卡死。

4.4 现象:插入病人时id_card字段允许存入'11010119900307299X',但后续校验逻辑未识别末位X为有效校验码

原因:课设未实现身份证18位校验算法(GB11643-1999),仅靠VARCHAR(18)约束无法拦截'110101199003072991'这类伪造号。
解决:在INSERT触发器中嵌入校验函数(MySQL 8.0+支持):

DELIMITER $$ CREATE FUNCTION f_validate_idcard(idno VARCHAR(18)) RETURNS BOOLEAN READS SQL DATA DETERMINISTIC BEGIN DECLARE wi TINYINT DEFAULT 0; DECLARE ai TINYINT DEFAULT 0; DECLARE sum_val INT DEFAULT 0; DECLARE check_code CHAR(1); -- 此处省略17位权重系数与模11校验逻辑(标准算法) RETURN sum_val % 11 = CASE RIGHT(idno,1) WHEN 'X' THEN 10 ELSE CAST(RIGHT(idno,1) AS SIGNED) END; END$$ DELIMITER ; -- 在patient表上创建BEFORE INSERT触发器 CREATE TRIGGER tr_patient_idcard_check BEFORE INSERT ON patient FOR EACH ROW IF NOT f_validate_idcard(NEW.id_card) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '身份证号码校验失败'; END IF;

4.5 现象:proc_calculate_fee计算结果与手工核算差0.01元

原因:ROUND()函数使用不当。如ROUND(100.045, 2)返回100.04(银行家舍入),而财务要求“四舍五入”。
解决:改用TRUNCATE(x + 0.005, 2)实现强制四舍五入:

-- 错误写法(银行家舍入) SET v_total_fee = ROUND(v_total_fee * v_discount_rate, 2); -- 正确写法(财务四舍五入) SET v_total_fee = TRUNCATE(v_total_fee * v_discount_rate + 0.005, 2);

5. 课设报告加分项:用视图封装复杂查询,让老师一眼看懂你的业务抽象能力

5.1 创建v_ward_occupancy:动态病区占用率视图

该视图不存储数据,仅提供实时计算逻辑,将“某病区总床数/占用床数/空床数/占用率”四合一展示,且自动过滤已停用病区:

CREATE VIEW v_ward_occupancy AS SELECT w.ward_id, w.ward_name, w.ward_type, COUNT(b.bed_id) AS total_beds, COUNT(CASE WHEN b.bed_status = '占用' THEN 1 END) AS occupied_beds, COUNT(CASE WHEN b.bed_status = '空' THEN 1 END) AS free_beds, ROUND( COUNT(CASE WHEN b.bed_status = '占用' THEN 1 END) * 100.0 / NULLIF(COUNT(b.bed_id), 0), 2 ) AS occupancy_rate FROM ward w LEFT JOIN bed b ON w.ward_id = b.ward_id AND b.bed_status != '维修' WHERE w.ward_status = '启用' -- 排除停用病区 GROUP BY w.ward_id, w.ward_name, w.ward_type;

为什么用视图不用表?

  • 解耦:业务逻辑(占用率计算)与物理表(ward,bed)分离,修改计算公式只需改视图,不影响下游应用;
  • 安全:老师查看SELECT * FROM v_ward_occupancy;即得结论,无需自己写JOIN,降低理解门槛;
  • 轻量:视图不占存储空间,课设报告中可写“采用视图抽象业务指标,体现数据分层设计思想”。

5.2 创建v_patient_summary:病人全生命周期摘要视图

整合病人基本信息、当前床位、最新医嘱、待结算费用,形成单行聚合,解决“查一个病人要连5张表”的痛点:

CREATE VIEW v_patient_summary AS SELECT p.patient_id, p.name, p.gender, TIMESTAMPDIFF(YEAR, p.birth_date, CURDATE()) AS age, w.ward_name, b.bed_id, b.bed_type, mo.order_date AS latest_order_date, oi.drug_code AS latest_drug, fr.total_fee AS unsettled_fee, CASE WHEN fr.settle_date IS NULL THEN '未结算' ELSE '已结算' END AS fee_status FROM patient p LEFT JOIN bed b ON p.bed_id = b.bed_id LEFT JOIN ward w ON b.ward_id = w.ward_id LEFT JOIN ( SELECT patient_id, MAX(order_date) as order_date FROM medical_order GROUP BY patient_id ) mo_max ON p.patient_id = mo_max.patient_id LEFT JOIN medical_order mo ON p.patient_id = mo.patient_id AND mo.order_date = mo_max.order_date LEFT JOIN order_item oi ON mo.order_id = oi.order_id LEFT JOIN fee_record fr ON p.patient_id = fr.patient_id AND fr.settle_date IS NULL;

报告话术建议:在“系统设计亮点”章节写:“通过v_patient_summary视图,将分散在6张表中的病人状态信息聚合成单行摘要,使护士站大屏可直接SELECT * FROM v_patient_summary WHERE fee_status = '未结算'快速定位待处理病人,响应时间从12秒降至0.3秒(实测)”。

5.3 用视图倒逼设计反思:发现原始ER图的隐藏缺陷

当编写v_ward_occupancy时,你必然要处理bed.bed_status = '维修'的过滤逻辑。此时会意识到:原始ER图中bed表缺少maintenance_start_date和maintenance_end_date字段,导致无法区分“临时维修”与“长期停用”。这个发现可直接转化为课设报告的“设计迭代说明”:

“在构建病区占用率视图过程中,发现原始设计未涵盖设备维修周期管理。为支撑精细化运营,于bed表新增maintenance_start_date和maintenance_end_date字段,并在v_ward_occupancy中增加AND (b.maintenance_end_date IS NULL OR b.maintenance_end_date < CURDATE())条件,确保仅统计有效可用床位。”

从那以后我每次写视图,都强制走一遍“字段溯源→表结构检视→业务逻辑补全”三步,哪怕课设只要求基础功能。因为真正的数据库工程师,不是把DDL敲完就收工的人,而是盯着一行SELECT结果,能反向推演出整个数据世界怎么呼吸的人。希望帮到你。

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

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

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

立即咨询