资讯动态

基于Python和MySQL的医院管理系统课设:数据库表设计、事务与并发挂号实战

发布时间:2026/10/9 15:41:43 来源:尧图企业网站定制
简介这份资源是面向计算机相关专业在校学生与初学者的医院管理系统完整项目包基于Python与MySQL开发可作为数据库课程设计、毕业设计或课程大作业的参考实现。项目围绕医院业务场景涵盖数据初始化、数据库连接、数据查询、数据字典、数据读写与操作等模块并配套建表、触发器、数据插入与初始化等SQL脚本便于理解数据库表结构设计与Python后端交互逻辑。压缩包共22个文件以py源码、pyc编译文件、sql脚本和xlsx数据表为主整体约43KB体积轻便下载后可直接运行测试。目前已有557人学习下载说明其作为课设参考具有一定认可度。读者可借此掌握从数据库建模到功能实现的完整流程并在此基础上修改扩展用于毕设、课设或项目初期演示。1. 从一份课设压缩包说起医院管理系统到底在练什么很多同学拿到「基于 Python 和 MySQL 的医院管理系统源码 SQL 数据库」这类课设题目时第一反应是去搜一份能跑的代码把环境配好、截图交差。但真正做过数据库课设的人会发现老师看的从来不是界面有多花哨而是你的表结构设计得合不合理、外键和事务有没有用对、并发挂号会不会把号重复发出去。这套系统本质上是一个「用 Python 当门面、用 MySQL 当大脑」的信息管理练习它把挂号、就诊、开药、收费这几条业务线串起来逼你去想清楚实体之间的关系。它适合两类人一类是正在做数据库课设、需要一份能跑通又能讲明白的参考实现的学生另一类是想拿一个完整小项目练手、把 SQL 从「会写 SELECT」提升到「会设计表、会写事务」的开发者。下面我按「先想清楚数据怎么存再动手把代码跑起来最后看坑在哪」的顺序把整套东西拆开讲。你照着做能复现一个能挂号、能开处方、能查账单的最小可用版本。2. 先把表设计对医院管理系统的数据库建模与建表脚本数据库课设翻车十有八九翻在表设计上。界面能跑不代表数据对等到要统计「某医生本月开了多少处方」时才发现字段没留那时候改表结构就是一场灾难。所以这一章先把建模讲透再给可直接执行的建表脚本。2.1 五张核心表撑起整条业务线一个最小可用的医院管理系统核心实体就那么几个科室、医生、患者、挂号记录、处方含处方明细。把它们的关系理清楚整个系统就立住了。科室department一个科室有多个医生是一对多。医生doctor属于某个科室一个医生可以接诊多个挂号。患者patient一个患者可以多次挂号是一对多。挂号registration连接患者和医生记录挂号时间、状态、费用是整条业务线的枢纽。处方prescription 处方明细prescription_item一次就诊可以开多张处方一张处方含多种药品所以处方和明细是一对多明细里再关联药品表。这里有个关键判断挂号和处方要分开建表不要塞进一张「就诊记录」里。因为挂号可能发生但患者没来就诊爽约也可能就诊了但没开药。合并成一张表状态字段会变得极其难维护查询时到处是 NULL 判断。分开建每张表职责单一这是血泪经验。药品表medicine单独建存药品名、单价、库存。处方明细只存药品 ID 和数量单价从药品表实时取或快照存一份这个后面避坑章节会专门讲。2.2 建表脚本与字段类型选择下面这份脚本可以直接在 MySQL 8 里执行。注意字符集统一用 utf8mb4不然患者姓名里的生僻字会变问号。-- 建库字符集必须 utf8mb4否则中文姓名可能乱码 CREATE DATABASE hospital_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE hospital_db; -- 科室表结构最简单先建它被医生表依赖 CREATE TABLE department ( dept_id INT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(50) NOT NULL UNIQUE, location VARCHAR(100) -- 科室位置可空 ) ENGINEInnoDB; -- 医生表外键指向科室删除科室时限制删除防止出现孤儿医生 CREATE TABLE doctor ( doctor_id INT PRIMARY KEY AUTO_INCREMENT, doctor_name VARCHAR(50) NOT NULL, dept_id INT NOT NULL, title VARCHAR(30), -- 职称主任医师/主治医师等 reg_fee DECIMAL(8,2) NOT NULL DEFAULT 0.00, -- 挂号费 CONSTRAINT fk_doctor_dept FOREIGN KEY (dept_id) REFERENCES department(dept_id) ON DELETE RESTRICT ) ENGINEInnoDB; -- 患者表身份证号唯一避免同一人重复建档 CREATE TABLE patient ( patient_id INT PRIMARY KEY AUTO_INCREMENT, patient_name VARCHAR(50) NOT NULL, id_card CHAR(18) NOT NULL UNIQUE, phone VARCHAR(20), gender TINYINT DEFAULT 0, -- 0未知 1男 2女 birth_date DATE ) ENGINEInnoDB; -- 挂号表业务枢纽状态字段决定后续流程能否推进 CREATE TABLE registration ( reg_id INT PRIMARY KEY AUTO_INCREMENT, patient_id INT NOT NULL, doctor_id INT NOT NULL, reg_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, reg_fee DECIMAL(8,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, -- 0已挂号 1已就诊 2已取消 CONSTRAINT fk_reg_patient FOREIGN KEY (patient_id) REFERENCES patient(patient_id), CONSTRAINT fk_reg_doctor FOREIGN KEY (doctor_id) REFERENCES doctor(doctor_id), INDEX idx_reg_doctor_time (doctor_id, reg_time) -- 按医生时间查号源 ) ENGINEInnoDB; -- 药品表库存和单价处方明细会引用 CREATE TABLE medicine ( med_id INT PRIMARY KEY AUTO_INCREMENT, med_name VARCHAR(80) NOT NULL, unit_price DECIMAL(8,2) NOT NULL, stock INT NOT NULL DEFAULT 0 ) ENGINEInnoDB; -- 处方主表一次就诊一张处方关联挂号 CREATE TABLE prescription ( pres_id INT PRIMARY KEY AUTO_INCREMENT, reg_id INT NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, CONSTRAINT fk_pres_reg FOREIGN KEY (reg_id) REFERENCES registration(reg_id) ) ENGINEInnoDB; -- 处方明细一张处方多条明细每条对应一种药 CREATE TABLE prescription_item ( item_id INT PRIMARY KEY AUTO_INCREMENT, pres_id INT NOT NULL, med_id INT NOT NULL, quantity INT NOT NULL, unit_price DECIMAL(8,2) NOT NULL, -- 快照单价防止药品调价影响历史账单 CONSTRAINT fk_item_pres FOREIGN KEY (pres_id) REFERENCES prescription(pres_id) ON DELETE CASCADE, CONSTRAINT fk_item_med FOREIGN KEY (med_id) REFERENCES medicine(med_id) ) ENGINEInnoDB;字段类型上有几个点值得说清楚。金额一律用DECIMAL而不是FLOAT浮点数算钱会出现0.1 0.2 0.30000000000000004这种玄学问题账单对不上就是从这里开始的。身份证号用CHAR(18)定长比VARCHAR省一点索引开销。状态字段用TINYINT加注释比存中文字符串「已就诊」更省空间也更好比较。prescription_item里的unit_price是刻意冗余的。药品表里的unit_price会随调价变化如果明细只存med_id那历史处方的金额会跟着变患者回头查账单发现金额对不上这就是典型的后悔药没处买。所以下单那一刻把单价快照进明细是账单类系统的通用做法。2.3 用视图和索引把常用查询提速课设数据量小索引看不出效果但老师往往会问「如果挂号表有十万条你怎么查」。这时候能答上索引和视图分数就上去了。-- 视图把挂号、患者、医生、科室拼成一张宽表前端直接查 CREATE VIEW v_registration_detail AS SELECT r.reg_id, p.patient_name, d.doctor_name, dept.dept_name, r.reg_time, r.reg_fee, r.status FROM registration r JOIN patient p ON r.patient_id p.patient_id JOIN doctor d ON r.doctor_id d.doctor_id JOIN department dept ON d.dept_id dept.dept_id; -- 复合索引按患者查历史挂号patient_id reg_time 覆盖排序 CREATE INDEX idx_reg_patient_time ON registration(patient_id, reg_time);视图的好处是把多表 JOIN 封装起来前端不用每次写四张表关联也降低了写错关联条件的概率。索引idx_reg_patient_time的顺序不能反patient_id在前是因为等值查询先过滤reg_time在后用于排序这个顺序符合最左前缀原则。如果你把reg_time放前面按患者查历史时索引就用不上了。3. 用 Python 把业务跑通连接、挂号与开处方的实现表建好了接下来是让 Python 去操作它。这一章给的是能直接跑的最小实现重点讲清楚连接怎么管、事务怎么用、并发挂号怎么防重。3.1 数据库连接与配置分离不要图省事把账号密码硬编码在每个函数里。抽一个配置模块用连接池管理连接这是从课设代码走向「像样工程」的第一步。# db.py —— 统一管理数据库连接 import pymysql from dbutils.pooled_db import PooledDB # 连接池避免每次请求都新建 TCP 连接课设也能用上 POOL PooledDB( creatorpymysql, maxconnections10, # 最大连接数课设 10 足够 mincached2, # 保持 2 个空闲连接 host127.0.0.1, port3306, userhospital_user, # 单独建的业务账号不要用 root passwordyour_password, databasehospital_db, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor # 返回字典取值更直观 ) def get_conn(): return POOL.connection()PooledDB来自dbutils库装的时候pip install pymysql dbutils。cursorclass设成DictCursor后查询结果每行是字典row[patient_name]比row[1]可读得多也避免了字段顺序变化导致的取值错位。生产环境账号权限要收窄业务账号只给SELECT/INSERT/UPDATEDROP、ALTER这类权限不给防止代码 bug 把表删了。3.2 挂号接口事务与并发防重挂号是整套系统里最需要小心的地方。两个窗口同时给同一个医生挂号如果不加控制可能把同一个号发两次或者号源超发。核心思路是「先查余量、再插入」这两步必须在一个事务里并且用行锁把医生这一行锁住。# registration_service.py from db import get_conn def register(patient_id: int, doctor_id: int) - dict: conn get_conn() try: conn.begin() # 开启事务 with conn.cursor() as cur: # 1. 锁定医生行防止并发下号源判断失效 cur.execute( SELECT reg_fee FROM doctor WHERE doctor_id%s FOR UPDATE, (doctor_id,) ) doc cur.fetchone() if not doc: conn.rollback() return {ok: False, msg: 医生不存在} # 2. 检查该患者是否已有未完成的挂号防止重复挂号 cur.execute( SELECT COUNT(*) AS cnt FROM registration WHERE patient_id%s AND doctor_id%s AND status0, (patient_id, doctor_id) ) if cur.fetchone()[cnt] 0: conn.rollback() return {ok: False, msg: 您已挂过该医生的号请勿重复} # 3. 插入挂号记录费用取医生表的挂号费 cur.execute( INSERT INTO registration(patient_id, doctor_id, reg_fee, status) VALUES(%s, %s, %s, 0), (patient_id, doctor_id, doc[reg_fee]) ) reg_id cur.lastrowid conn.commit() return {ok: True, reg_id: reg_id} except Exception as e: conn.rollback() # 任何异常都回滚保证数据一致 return {ok: False, msg: str(e)} finally: conn.close() # 归还连接到池FOR UPDATE是关键它给医生那一行加了排他锁第二个并发请求会在这里排队等待等第一个事务提交后才继续从而避免两个请求同时读到「可挂号」然后都插入。begin()和commit()之间任何一步失败都要rollback()否则会出现「挂号记录插了但费用没扣」这类脏数据。finally里close()是把连接还给池不是真正关闭 TCP这个习惯要养成不然连接池很快被耗尽。3.3 开处方主从表插入与金额计算开处方要同时写处方主表和明细表还要扣库存、算总金额这几步必须在一个事务里完成任何一步失败全部回滚。# prescription_service.py from db import get_conn def create_prescription(reg_id: int, items: list) - dict: items 形如 [{med_id: 1, quantity: 2}, ...] conn get_conn() try: conn.begin() with conn.cursor() as cur: total 0.0 # 先插入处方主表拿到 pres_id cur.execute( INSERT INTO prescription(reg_id, total_amount) VALUES(%s, 0), (reg_id,) ) pres_id cur.lastrowid for it in items: # 查药品当前单价和库存加行锁防止超卖 cur.execute( SELECT unit_price, stock FROM medicine WHERE med_id%s FOR UPDATE, (it[med_id],) ) med cur.fetchone() if not med: raise ValueError(f药品 {it[med_id]} 不存在) if med[stock] it[quantity]: raise ValueError(f药品 {it[med_id]} 库存不足) # 扣库存 cur.execute( UPDATE medicine SET stock stock - %s WHERE med_id%s, (it[quantity], it[med_id]) ) # 插入明细单价做快照 cur.execute( INSERT INTO prescription_item(pres_id, med_id, quantity, unit_price) VALUES(%s, %s, %s, %s), (pres_id, it[med_id], it[quantity], med[unit_price]) ) total float(med[unit_price]) * it[quantity] # 回填处方总金额 cur.execute( UPDATE prescription SET total_amount%s WHERE pres_id%s, (total, pres_id) ) conn.commit() return {ok: True, pres_id: pres_id, total: total} except Exception as e: conn.rollback() return {ok: False, msg: str(e)} finally: conn.close()这里FOR UPDATE锁的是药品行防止两个处方同时扣同一批库存导致超卖。金额累加用 Python 的float只是演示严谨做法是用decimal.Decimal或者干脆在 SQL 里用SUM算避免浮点误差。total_amount先插 0 再回填是因为插入主表时还不知道明细总额这是主从表写入的常见顺序。4. 避坑与排查课设里最容易翻车的五个地方代码能跑不等于没问题。下面这五条是我在类似项目里反复见到的坑每条都按「现象 → 原因 → 解决」说清楚。4.1 中文姓名变问号或乱码现象患者姓名存进去是???或者查询出来是乱码。原因通常是建库时字符集用了默认的latin1或者 Python 连接没指定charsetutf8mb4。解决建库语句显式写DEFAULT CHARACTER SET utf8mb4连接参数里加charsetutf8mb4两处都要对。已经建错的库用ALTER DATABASE hospital_db CHARACTER SET utf8mb4改但已有表的列字符集要单独ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4。4.2 外键约束导致删数据失败现象想删一个科室报Cannot delete or update a parent row。原因是医生表的外键设了ON DELETE RESTRICT科室下还有医生。这不是 bug是保护机制。解决要么先把该科室的医生转移或删除要么把外键改成ON DELETE SET NULL前提是dept_id允许为空。课设里建议保留RESTRICT答辩时能讲清楚「为什么不允许级联删除」反而是加分项。4.3 事务没提交数据「消失」了现象代码里INSERT执行了print也打了但去数据库查不到。原因是开了事务但忘了commit()或者异常分支里rollback()把数据撤了。解决检查每个写操作后面有没有commit()异常处理里rollback()是否误伤。用连接池时尤其注意连接归还前没提交事务会被回滚。养成「写操作必配 commit异常必配 rollback」的固定套路。4.4 并发挂号出现重复号现象压测或两个窗口同时操作时同一患者同一医生出现两条status0的挂号。原因是「查重」和「插入」之间没有锁两个请求都查完发现没有然后都插入。解决如 3.2 所示用SELECT ... FOR UPDATE锁住医生行把查重和插入放进同一个事务。更彻底的做法是在registration表上加唯一索引UNIQUE(patient_id, doctor_id, status)但这样会限制同一患者多次挂同一医生复诊所以更推荐事务加锁方案。4.5 金额用 FLOAT 导致对账差几分钱现象处方总金额和明细累加对不上差 0.01。原因是FLOAT/DOUBLE是二进制浮点无法精确表示 0.1 这类十进制小数。解决所有金额字段用DECIMAL(8,2)或DECIMAL(10,2)Python 侧用decimal.Decimal计算不要用float。已经用FLOAT的表ALTER TABLE ... MODIFY COLUMN unit_price DECIMAL(8,2)改过来历史数据重新算一遍。5. 让课设更像样从能跑到能讲清楚的三个进阶技巧前面把系统跑通了但课设答辩和实际使用之间还有一段距离。这一章讲三个能让你的项目从「能跑」变成「能讲清楚、能扛住问」的技巧都是我踩过坑之后固定下来的习惯。第一个技巧是给关键查询加 EXPLAIN 并会读结果。老师问「你这个查询慢不慢」你不能只说「数据少所以快」。拿挂号明细视图举例执行EXPLAIN SELECT * FROM v_registration_detail WHERE patient_name张三看type列是不是ALL全表扫描rows列估算扫了多少行。如果是ALL且行数大说明patient_name没索引加一个CREATE INDEX idx_patient_name ON patient(patient_name)。会看type、key、rows这三列基本就能应付大部分性能追问。注意视图的 EXPLAIN 有时会展开成子查询看不清楚就直接 EXPLAIN 底层 JOIN 语句。第二个技巧是用存储过程或触发器处理「挂号后自动建就诊记录」这类联动。课设里加一个触发器能体现你对数据库能力的掌握。比如患者挂号后自动在日志表记一笔-- 挂号日志表 CREATE TABLE reg_log ( log_id INT PRIMARY KEY AUTO_INCREMENT, reg_id INT, action VARCHAR(20), log_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 触发器挂号插入后自动写日志 DELIMITER // CREATE TRIGGER trg_reg_after_insert AFTER INSERT ON registration FOR EACH ROW BEGIN INSERT INTO reg_log(reg_id, action) VALUES(NEW.reg_id, CREATE); END // DELIMITER ;NEW.reg_id指刚插入那行的reg_idAFTER INSERT保证主表数据已落库。触发器适合做日志、审计这类旁路逻辑但不要把核心业务塞进触发器否则调试时逻辑藏在数据库里成了黑匣子出问题很难查。这是边界要清楚。第三个技巧是把配置和敏感信息抽到环境变量。课设代码里写死密码很常见但答辩时被问「密码怎么管理」就尴尬了。用os.environ读环境变量配一个.env文件不提交到版本库代码里passwordos.environ.get(DB_PASSWORD)。这样既显得规范也避免了把密码贴进报告里。再进一步可以给不同环境准备不同配置本地用一套、演示用一套切换时只改环境变量不改代码。最后说个我自己的习惯每次改完表结构一定先mysqldump备份一份再动手。课设阶段数据不重要但这个动作能让你在改坏表之后有后悔药吃而不是重建整个库。mysqldump -u hospital_user -p hospital_db backup_$(date %Y%m%d).sql一行命令的事养成习惯比什么都强。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑