简介:这份《医院病房管理系统--数据库课设》资源面向高校计算机与信息管理专业学生,用于完成数据库课程设计或作为Java桌面应用开发练手项目。系统围绕病人入院登记、病房与床位分配、医护人员排班、诊疗记录及费用结算等真实医院业务流程展开,帮助读者把关系数据库理论、SQL语言与B/S或C/S架构开发结合起来。压缩包共146个文件,约5.04MB,以59个class与37个java源码为核心,配合24张jpg与6张png界面截图、4个xml配置、1个sql建表脚本及doc说明文档,完整呈现从数据库设计到界面实现的工程结构。目前已有165人学习下载。读者可参考其中的实体关系设计、主外键与索引运用、增删改查逻辑及权限与隐私保护思路,快速搭建可运行的管理系统原型,并据此撰写课设报告与答辩材料。
1. 病房管理系统数据库课设:从建表到能跑通的最小闭环
很多同学做数据库课设,第一反应是打开 IDE 写 Java 界面,结果界面画得花里胡哨,数据库里就三张表,外键都没连上,答辩时老师一句“你这个并发怎么处理”直接卡壳。病房管理系统的数据库课设,核心从来不是界面,而是数据模型能不能撑住真实业务:一个病人从入院到出院,中间要经历床位分配、医嘱下达、护理记录、费用结算,每一步都是表与表之间的约束在兜底。这个标题对应的场景很具体——医院病房的日常运转,涉及病人、床位、医生、护士、医嘱、费用这几类实体,适合数据库课程设计、SQL 综合训练、以及想补关系型建模实战的开发者。我见过太多课设把“病房管理”做成增删改查演示,真正拉开差距的是:你的表结构能不能防止一个床位同时住两个人,能不能追溯某条医嘱是谁在什么时候改的。这篇笔记就按“能复现、能答辩、能扛住追问”的标准,把整套数据库方案拆开讲。
2. 需求拆解与实体关系:先画清楚再动手建表
2.1 病房管理里到底有哪几张核心表
做课设最容易翻车的地方,是拿到题目直接开写CREATE TABLE,写到一半发现少了个字段,回头改表结构,外键全乱。我一般会先花二十分钟把实体列全,再动手。病房管理系统的实体不算多,但关系不简单:
- 病人(patient):住院号、姓名、性别、身份证号、联系电话、入院时间、出院时间、状态。
- 病房(ward):病房号、楼层、类型(普通/重症/隔离)、床位数。
- 床位(bed):床位号、所属病房、状态(空闲/占用/维修)。
- 医生(doctor):工号、姓名、科室、职称。
- 护士(nurse):工号、姓名、所属病区、班次。
- 住院记录(admission):病人、床位、主治医生、入院时间、出院时间,这是核心业务表。
- 医嘱(medical_order):住院记录、开嘱医生、医嘱内容、开嘱时间、执行状态。
- 护理记录(nursing_record):住院记录、执行护士、护理内容、记录时间。
- 费用明细(fee_detail):住院记录、费用类型、金额、产生时间。
这九张表基本覆盖了课设答辩会问到的所有业务点。注意admission是枢纽表,它把病人、床位、医生串起来,医嘱和护理记录都挂在它下面,而不是直接挂病人。这个设计的好处是:一个病人多次住院时,历史记录不会混。
2.2 用 ER 思路确定一对多和多对多
关系型建模的关键是判断基数。病房系统里绝大多数是一对多,但有两处容易搞错:
第一,病人和床位。直觉上是一个病人对一个床位,但病人出院后床位释放,新病人再入住,所以病人和床位是多对多,通过admission表来体现。如果你把床位号直接塞进病人表,历史记录就丢了,这是课设里最常见的建模错误。
第二,医生和病房。一个医生可以管多个病房,一个病房也可以有多个医生轮班,这是多对多,需要一张doctor_ward中间表。很多课设忽略这点,导致“查某个病房有哪些医生”时只能硬编码。
下面这张表是我建议的字段和约束对照,建表前先过一遍:
| 表名 | 主键 | 关键外键 | 必须有的约束 |
|---|---|---|---|
| patient | patient_id | 无 | 身份证号唯一 |
| ward | ward_id | 无 | 床位数 > 0 |
| bed | bed_id | ward_id | 床位状态枚举 |
| admission | admission_id | patient_id, bed_id, doctor_id | 出院时间晚于入院时间 |
| medical_order | order_id | admission_id, doctor_id | 状态枚举 |
| fee_detail | fee_id | admission_id | 金额 >= 0 |
提示:课设里把枚举值写成
ENUM还是TINYINT加字典表,取决于老师是否要求“可扩展”。我一般用TINYINT加注释,答辩时能多说一句“方便后续加状态”。
2.3 从 ER 到建表语句的落地顺序
确定实体和关系后,建表顺序必须遵守外键依赖:先建无外键的表,再建依赖表。下面是 MySQL 8.0 下可执行的建表脚本,字符集用utf8mb4,引擎InnoDB,因为要支持事务和外键。
-- 病人表:身份证号唯一,状态用 0/1/2 表示在院/出院/转院 CREATE TABLE patient ( patient_id BIGINT PRIMARY KEY AUTO_INCREMENT, id_card VARCHAR(18) NOT NULL UNIQUE COMMENT '身份证号', name VARCHAR(50) NOT NULL, gender TINYINT NOT NULL COMMENT '0女1男', phone VARCHAR(20), status TINYINT NOT NULL DEFAULT 0 COMMENT '0在院1出院2转院', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 病房表:床位数必须为正 CREATE TABLE ward ( ward_id INT PRIMARY KEY AUTO_INCREMENT, ward_no VARCHAR(10) NOT NULL UNIQUE, floor INT NOT NULL, ward_type TINYINT NOT NULL COMMENT '0普通1重症2隔离', bed_count INT NOT NULL CHECK (bed_count > 0) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 床位表:外键指向病房,状态枚举 CREATE TABLE bed ( bed_id INT PRIMARY KEY AUTO_INCREMENT, ward_id INT NOT NULL, bed_no VARCHAR(10) NOT NULL, bed_status TINYINT NOT NULL DEFAULT 0 COMMENT '0空闲1占用2维修', CONSTRAINT fk_bed_ward FOREIGN KEY (ward_id) REFERENCES ward(ward_id), UNIQUE KEY uk_ward_bed (ward_id, bed_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这段脚本里有两个细节值得说。uk_ward_bed唯一索引保证同一病房内床位号不重复,这是业务约束落到数据库层。CHECK (bed_count > 0)在 MySQL 8.0.16 之后才真正生效,低版本会忽略,答辩时如果老师问“你的约束真的起作用吗”,要能答出这个版本边界。
admission表是核心,建的时候要把入院出院时间约束加上:
CREATE TABLE admission ( admission_id BIGINT PRIMARY KEY AUTO_INCREMENT, patient_id BIGINT NOT NULL, bed_id INT NOT NULL, doctor_id INT NOT NULL, admit_time DATETIME NOT NULL, discharge_time DATETIME, CONSTRAINT fk_adm_patient FOREIGN KEY (patient_id) REFERENCES patient(patient_id), CONSTRAINT fk_adm_bed FOREIGN KEY (bed_id) REFERENCES bed(bed_id), CONSTRAINT fk_adm_doctor FOREIGN KEY (doctor_id) REFERENCES doctor(doctor_id), CONSTRAINT chk_discharge CHECK (discharge_time IS NULL OR discharge_time > admit_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;chk_discharge这个检查约束能挡住“出院时间早于入院时间”的脏数据,课设里加上它,答辩时是一个加分点。注意discharge_time允许为 NULL,表示病人还在院,这比用空字符串优雅得多。
3. 让数据动起来:存储过程、触发器与并发占床
3.1 用存储过程封装入院和出院两个核心动作
课设如果只写几条INSERT,老师大概率会问“业务逻辑放哪”。把入院和出院做成存储过程,既体现数据库编程能力,又能保证原子性。入院存储过程要做三件事:检查床位是否空闲、插入住院记录、把床位改成占用。三步必须在一个事务里。
DELIMITER // CREATE PROCEDURE admit_patient( IN p_patient_id BIGINT, IN p_bed_id INT, IN p_doctor_id INT ) BEGIN DECLARE v_status TINYINT; -- 开启事务,保证占床和插记录一致 START TRANSACTION; SELECT bed_status INTO v_status FROM bed WHERE bed_id = p_bed_id FOR UPDATE; IF v_status IS NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '床位不存在'; ELSEIF v_status <> 0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '床位已被占用或维修中'; ELSE INSERT INTO admission(patient_id, bed_id, doctor_id, admit_time) VALUES (p_patient_id, p_bed_id, p_doctor_id, NOW()); UPDATE bed SET bed_status = 1 WHERE bed_id = p_bed_id; UPDATE patient SET status = 0 WHERE patient_id = p_patient_id; COMMIT; END IF; END // DELIMITER ;这里SELECT ... FOR UPDATE是行级锁,防止两个并发请求同时读到“空闲”然后都去占床。SIGNAL SQLSTATE '45000'是自定义错误,调用方能在应用层捕获。参数p_patient_id、p_bed_id、p_doctor_id都是外部传入,存储过程本身不生成主键,交给自增。
出院存储过程逻辑相反,但要多一步:把该住院记录下未执行的医嘱标记为作废。
DELIMITER // CREATE PROCEDURE discharge_patient(IN p_admission_id BIGINT) BEGIN DECLARE v_bed_id INT; START TRANSACTION; SELECT bed_id INTO v_bed_id FROM admission WHERE admission_id = p_admission_id; UPDATE admission SET discharge_time = NOW() WHERE admission_id = p_admission_id; UPDATE bed SET bed_status = 0 WHERE bed_id = v_bed_id; UPDATE medical_order SET order_status = 3 WHERE admission_id = p_admission_id AND order_status = 0; COMMIT; END // DELIMITER ;order_status = 3表示作废,0 表示待执行。这个细节很多课设漏掉,导致病人出院后还有“待执行医嘱”挂在系统里,答辩时被追问就很尴尬。
3.2 触发器兜住床位状态,别只靠应用层
存储过程能管住走存储过程的调用,但如果有人直接UPDATE bed,状态还是会乱。触发器的作用是兜底。我一般会在admission表上建一个AFTER INSERT触发器,自动把床位改成占用,这样即使应用层忘了改,数据库也不会出现“住院记录有了但床位还显示空闲”的玄学问题。
DELIMITER // CREATE TRIGGER trg_admission_after_insert AFTER INSERT ON admission FOR EACH ROW BEGIN UPDATE bed SET bed_status = 1 WHERE bed_id = NEW.bed_id; END // DELIMITER ;NEW.bed_id是刚插入行的床位号。注意触发器和存储过程里的UPDATE bed会重复执行,但因为是幂等的(都设成 1),不会出错。如果你追求干净,可以把存储过程里的UPDATE bed去掉,只留触发器。我倾向留触发器,因为它是最后一道防线。
3.3 并发占床的两种锁策略对比
课设答辩高频问题:“两个护士同时给同一个床位办入院怎么办?”答案就在锁。下面这张表是我实际压测过的两种策略:
| 策略 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| 悲观锁 | SELECT ... FOR UPDATE | 逻辑简单,强一致 | 并发高时锁等待 |
| 乐观锁 | 版本号字段 +UPDATE ... WHERE version = ? | 无锁等待 | 失败要重试,代码复杂 |
课设规模用悲观锁足够,FOR UPDATE配合InnoDB行锁,几十个并发没问题。但要注意:FOR UPDATE必须在事务里,且查询条件要命中索引,否则会升级成表锁。bed_id是主键,天然命中,这点可以放心。
4. 查询与报表:课设答辩最容易被追问的 SQL
4.1 查空闲床位:一个容易被写错的简单查询
“查某病房当前空闲床位”看起来简单,但很多人写成:
-- 错误写法:只查 bed 表,忽略了住院记录 SELECT * FROM bed WHERE ward_id = 1 AND bed_status = 0;如果触发器或存储过程有 bug,bed_status可能不准。更稳的写法是关联admission判断:
SELECT b.bed_id, b.bed_no FROM bed b LEFT JOIN admission a ON b.bed_id = a.bed_id AND a.discharge_time IS NULL WHERE b.ward_id = 1 AND a.admission_id IS NULL;LEFT JOIN加a.admission_id IS NULL表示“没有在院记录”,这比信任bed_status更可靠。discharge_time IS NULL是在院标志。这个查询在答辩时能体现你对数据一致性的理解。
4.2 统计医生工作量:分组加条件聚合
“每个医生当前管了多少病人”是经典报表题。用COUNT加CASE WHEN可以一次查出在院和累计两个指标:
SELECT d.doctor_id, d.name, COUNT(CASE WHEN a.discharge_time IS NULL THEN 1 END) AS current_patients, COUNT(a.admission_id) AS total_patients FROM doctor d LEFT JOIN admission a ON d.doctor_id = a.doctor_id GROUP BY d.doctor_id, d.name;COUNT(CASE WHEN ... THEN 1 END)只统计在院,COUNT(a.admission_id)统计累计。注意LEFT JOIN保证没病人的医生也出现,COUNT对 NULL 不计数,所以结果是 0 而不是漏行。
4.3 费用汇总:用窗口函数算累计费用
MySQL 8.0 支持窗口函数,课设里用上能加分。“每个住院记录的费用明细按时间累计”:
SELECT admission_id, fee_type, amount, fee_time, SUM(amount) OVER (PARTITION BY admission_id ORDER BY fee_time) AS running_total FROM fee_detail ORDER BY admission_id, fee_time;PARTITION BY admission_id按住院记录分组,ORDER BY fee_time决定累计顺序。这个查询在费用结算页面很有用,能显示“截至当前时刻已产生多少费用”。
注意:窗口函数在 MySQL 8.0 以下不可用,如果实验室环境是 5.7,要改成自连接或变量写法,答辩前先确认版本。
5. 避坑与排查:课设里最容易翻车的五个点
5.1 外键约束导致插入顺序报错
现象:插入admission时报Cannot add or update a child row。原因:patient_id或bed_id在父表里不存在,或者父表插入还没提交。解决:按patient → ward → bed → doctor → admission顺序插入,且确认父表数据已COMMIT。批量初始化时用SET FOREIGN_KEY_CHECKS = 0临时关闭,但导入完必须开回来。
5.2 触发器里查同一张表导致死锁
现象:插入admission时卡住,日志显示锁等待。原因:触发器里UPDATE bed,而外层事务可能已经锁了bed行,形成循环等待。解决:触发器里避免复杂查询,只做简单UPDATE;或者把占床逻辑统一放存储过程,触发器只做兜底且不加FOR UPDATE。
5.3 字符集不统一导致中文乱码
现象:病人姓名存进去变成问号。原因:建表用utf8mb4,但连接串没指定字符集。解决:连接 URL 加?characterEncoding=utf8&useUnicode=true,同时确认my.ini里character-set-server=utf8mb4。课设里姓名乱码是高频扣分点。
5.4 出院时间约束被忽略
现象:能插入出院时间早于入院时间的记录。原因:CHECK约束在低版本 MySQL 被静默忽略。解决:升级到 8.0.16+,或者在存储过程里手动判断IF p_discharge_time <= admit_time THEN SIGNAL ...。我一般双保险,约束加存储过程判断。
5.5 并发测试时数据对不上
现象:用脚本模拟 50 个并发入院,最后发现床位占用数和住院记录数不一致。原因:存储过程里SELECT ... FOR UPDATE没加,或者事务隔离级别是READ COMMITTED导致幻读。解决:确认隔离级别为REPEATABLE READ(MySQL 默认),存储过程里加FOR UPDATE,并用SHOW ENGINE INNODB STATUS看锁情况。
6. 进阶技巧:把课设做成能写进简历的项目
课设做完能跑只是及格,想让它成为简历上能聊的东西,得再加一层。我一般会做三件事:加审计日志、加索引优化、加一个简单的压测脚本。
审计日志用触发器实现,记录谁改了医嘱:
CREATE TABLE order_audit ( audit_id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT, old_status TINYINT, new_status TINYINT, changed_at DATETIME DEFAULT CURRENT_TIMESTAMP ); DELIMITER // CREATE TRIGGER trg_order_audit AFTER UPDATE ON medical_order FOR EACH ROW BEGIN IF OLD.order_status <> NEW.order_status THEN INSERT INTO order_audit(order_id, old_status, new_status) VALUES (OLD.order_id, OLD.order_status, NEW.order_status); END IF; END // DELIMITER ;OLD和NEW是触发器里的关键字,分别代表更新前和更新后的行。IF判断只在状态真变了才记录,避免无意义日志。这个审计表在答辩时能讲“可追溯性”,是医院信息系统的硬需求。
索引优化方面,admission表上patient_id、bed_id、discharge_time都该建索引。特别是查“某病人历史住院记录”和“当前在院病人”这两个高频查询,没索引全表扫,数据量一大就慢。用EXPLAIN看执行计划,确认type不是ALL。
压测脚本用 Python 写,模拟并发入院:
import threading import pymysql def admit(patient_id, bed_id): conn = pymysql.connect(host='localhost', user='root', password='yourpass', database='ward') try: with conn.cursor() as cur: cur.callproc('admit_patient', (patient_id, bed_id, 1)) conn.commit() except Exception as e: print(f'failed: {e}') finally: conn.close() threads = [threading.Thread(target=admit, args=(i, 1)) for i in range(1, 51)] for t in threads: t.start() for t in threads: t.join()这段脚本起 50 个线程抢同一个床位,预期只有 1 个成功,其余报“床位已被占用”。如果成功数大于 1,说明锁没生效,回去检查存储过程。callproc调用存储过程,参数按顺序传。跑完查admission表,应该只有一条记录对应bed_id = 1且discharge_time IS NULL。
最后说个血泪经验:课设文档里一定要写清楚“为什么这么设计”,而不是只贴代码。老师问“你为什么用触发器不用应用层逻辑”,你要能答“触发器保证任何写入路径都受约束,应用层可能被绕过”。我当年第一次答辩就是只讲功能不讲取舍,被追问到哑口无言。后来养成习惯,每张表、每个约束都准备一句“为什么”,答辩就稳了。希望帮到你。
本文还有配套的精品资源,点击获取