
简介面向Python开发学习与医院信息系统课程设计这份资源以实际小组作业为载体系统展示基于Python的HIS从数据库设计到前后端实现的完整流程适合正在完成信息系统大作业或希望了解Web后端开发的学生参考。资源围绕患者管理、医生排班、药品库存、财务结算等核心业务展开配合SQL数据脚本与Vue前端页面呈现可运行的完整项目骨架。压缩包共67个文件包含21个vue页面、14个py后端脚本、8个js逻辑文件及4个sql数据脚本整体大小仅497KB结构紧凑便于快速下载与本地部署。目前已有428人学习。另附README、manage.py启动入口及前后端目录可据此还原环境梳理各业务模块的数据流与接口设计便于课程设计提交、答辩讲解或Web开发进阶参考。1. 信息系统小组作业里Python医院信息系统才是硬骨头信息系统小组作业里最常出现的场景是一个 Python 医院信息系统的 zip 包发到群里后半天没人接手。真正难的往往不是“写代码”而是把挂号、就诊、收费、退号这条业务闭环跑通并且在答辩时经得起老师随手点几个按钮。医院信息系统这个方向并不是要你把三甲医院的 HIS 复刻出来它的核心价值在于用最少的功能点把“患者从进门到离开”的完整数据流讲清楚。这篇笔记适合正在做课程设计的小组也适合有 Python 基础、想快速补一套 HIS 底稿的开发新人。看完你能得到一个能跑通的方案同时避开小组作业里最容易翻车的几个坑。2. 先想清楚边界再动手HIS 的选型与模块取舍2.1 第一版一般先写透哪几个模块商业 HIS 包含门诊、住院、药房、检验、检查、手术、病案、医保结算等几十个子系统小组作业如果照这个清单铺开两个月也写不完。常见做法是只做门诊这条主链路把“挂号—医生看诊—开药—收费—退号”作为业务闭环其余模块做成视图或报表即可。这个方向的核心是让老师看到系统里每一张表之间存在真实的外键关联而不是一堆孤立的表格。我一般会把功能切成四个块患者与挂号、医生工作站、药房与药品、收费与统计。患者与挂号负责建档、选择科室医生、生成挂号记录医生工作站负责查看当日患者、录入诊断、开处方药房负责药品库存和发药扣减收费与统计负责缴费、退费以及按天/按科室汇总金额。这样每个组员都有独立任务又能通过数据库关联起来避免多人同时改同一个文件。功能清单可以这样定义挂号模块至少要有科室选择、医生选择、号源校验医生模块至少要有诊断录入和处方保存药品模块要有库存扣减和库存预警收费模块要有待缴费列表、收费确认和退号退费。四个模块里挂号加收费是核心中的核心老师大概率会先从这两个地方开始按后面的处方和药品只是让数据流变得更完整。2.2 数据库选型SQLite 让小组少搭一台服务小组作业的交付物是一个 zip 包这意味着评审老师可能在任何一台电脑上解压运行。如果你选了 MySQL 还要现场搭服务、建库、配账号风险会成倍增加。SQLite 是这类项目的首选单文件、零配置、Python 标准库自带驱动不需要单独安装数据库服务。你只需要在代码里写下sqlite3.connect(his.db)程序第一次运行就会自动创建数据库文件这一点在答辩现场是巨大的优势。MySQL 也不是不能用前提是团队里有人能搞定安装和权限配置并且演示电脑的网络环境可控。常见的小组作业场景里我是不推荐主动上 MySQL 的因为线上评审时数据库服务没启动的概率远高于你想象。还有一个原因SQLite 的 SQL 语法和 MySQL 90% 以上相同如果以后想迁移改动量并不大。一开始就把数据库访问封装到一个模块里不要在每个页面里到处connect。我一般会建一个db.py里面统一管理连接、游标、事务提交和回滚其他模块只调用函数。这个习惯能让你的代码在后续联调时少走很多弯路也让组员之间不需要互相询问“你那个库连的是哪个文件”。2.3 界面层tkinter 还是 Web 框架界面选型决定小组的工作方式。tkinter 是 Python 自带 GUI 库优点是零依赖、打包简单缺点是布局调试比较费眼神做出来的界面风格偏原生。Flask 加 Jinja2 模板是另一个常见路线浏览器里渲染样式用 Bootstrap 就能做得很像样前后端分离也方便分工。如果小组里有人用过 Vue也可以把它和 Flask 组合但这也意味着多一层构建和跨域配置对小组作业来说往往得不偿失。先看组员底子再定方案全员只会 Python 基础就选 tkinter有人写过 HTML就选 Flask。三人以上的小组我一般会建议走 Web 方向因为前端做页面、后端写接口分工清晰git 合并时冲突也少。VSCode 配 Python 环境是常规操作但不要让组员在环境配置上花超过半天时间否则后面联调时间一定不够用。还需要考虑演示效果tkinter 程序在窗口里点按钮Flask 程序在浏览器里刷新页面、提交表单后者的视觉反馈更直观也更容易用录像工具录成演示视频。不过 Web 方向要多处理一个端口占用问题答辩前记得检查 5000 端口是否被其他进程占用否则启动失败会很尴尬。3. 把系统跑起来建库、核心逻辑与数据流可复现3.1 建表脚本与初始化数据先写好最小可用表动手写功能之前先把数据库表结构定下来。下面的脚本可以在 SQLite 里创建一张最小但完整的门诊业务表覆盖患者、医生、挂号、处方、药品、支付六个核心对象。注意外键约束要打开否则后面的数据一致性没法保证。PRAGMA foreign_keys ON; CREATE TABLE IF NOT EXISTS departments ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE ); CREATE TABLE IF NOT EXISTS doctors ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, title TEXT, department_id INTEGER NOT NULL, FOREIGN KEY (department_id) REFERENCES departments(id) ); CREATE TABLE IF NOT EXISTS patients ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, gender TEXT CHECK(gender IN (男, 女)), age INTEGER, id_card TEXT UNIQUE, phone TEXT ); CREATE TABLE IF NOT EXISTS registrations ( id INTEGER PRIMARY KEY AUTOINCREMENT, patient_id INTEGER NOT NULL, doctor_id INTEGER NOT NULL, reg_date TEXT NOT NULL, status TEXT DEFAULT 待缴费, amount REAL DEFAULT 0, FOREIGN KEY (patient_id) REFERENCES patients(id), FOREIGN KEY (doctor_id) REFERENCES doctors(id) ); CREATE TABLE IF NOT EXISTS drugs ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, spec TEXT, price REAL NOT NULL, stock INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS prescriptions ( id INTEGER PRIMARY KEY AUTOINCREMENT, registration_id INTEGER NOT NULL, drug_id INTEGER NOT NULL, quantity INTEGER NOT NULL, amount REAL NOT NULL, FOREIGN KEY (registration_id) REFERENCES registrations(id), FOREIGN KEY (drug_id) REFERENCES drugs(id) ); CREATE TABLE IF NOT EXISTS payments ( id INTEGER PRIMARY KEY AUTOINCREMENT, registration_id INTEGER NOT NULL, amount REAL NOT NULL, pay_time TEXT NOT NULL, pay_type TEXT, FOREIGN KEY (registration_id) REFERENCES registrations(id) );这里有几个字段值得单独说明patients.id_card设置 UNIQUE 是为了防止同一个人建档两次后续新建患者时先查身份证号有就直接返回已有 IDregistrations.status用默认值待缴费后续通过更新这个字段来表示缴费和退号而不是删除记录——保留痕迹在答辩时更容易讲清楚prescriptions.amount存的是处方单行金额快照即使以后药品价格调整历史数据也不会变。建表之后写初始化数据否则医生下拉框是空的。初始化脚本可以单独放在init_data.py里用INSERT INTO departments(name) VALUES (内科), (外科), (儿科);这类语句插入科室再插入几个医生、药品和一条管理员账户。注意插入顺序依赖外键先科室、再医生、再药品否则会报外键约束错误。3.2 核心业务逻辑挂号、缴费、退号怎么用 Python 写后端的核心在三个操作挂号、缴费、退号。这里给出一个可以直接运行的service.py用 Python 函数把这三个业务封装好。函数式拆分的优点在于方便单元测试也和后面做 Flask 接口一一对应。import sqlite3 from datetime import datetime DB_PATH his.db def get_conn(): conn sqlite3.connect(DB_PATH) conn.row_factory sqlite3.Row conn.execute(PRAGMA foreign_keys ON) return conn def find_or_create_patient(conn, name, gender, age, id_card, phone): row conn.execute( SELECT id FROM patients WHERE id_card ?, (id_card,) ).fetchone() if row: return row[id] cur conn.execute( INSERT INTO patients(name, gender, age, id_card, phone) VALUES (?,?,?,?,?), (name, gender, age, id_card, phone) ) return cur.lastrowid def register(conn, patient_id, doctor_id): today datetime.now().strftime(%Y-%m-%d) exists conn.execute( SELECT id FROM registrations WHERE doctor_id ? AND reg_date LIKE ? AND status ! 已退, (doctor_id, today %) ).fetchone() if exists: raise ValueError(该医生今天已有挂号记录) cur conn.execute( INSERT INTO registrations(patient_id, doctor_id, reg_date, status, amount) VALUES (?,?,?,?,?), (patient_id, doctor_id, datetime.now().strftime(%Y-%m-%d %H:%M:%S), 待缴费, 20) ) return cur.lastrowid def pay(conn, registration_id, pay_type现金): reg conn.execute( SELECT * FROM registrations WHERE id ?, (registration_id,) ).fetchone() if not reg or reg[status] 已退: raise ValueError(挂号记录不存在或已退) if reg[status] 已缴费: raise ValueError(该号已缴费不能重复支付) conn.execute( UPDATE registrations SET status 已缴费 WHERE id ?, (registration_id,) ) conn.execute( INSERT INTO payments(registration_id, amount, pay_time, pay_type) VALUES (?,?,?,?), (registration_id, reg[amount], datetime.now().strftime(%Y-%m-%d %H:%M:%S), pay_type) ) def refund(conn, registration_id): reg conn.execute( SELECT * FROM registrations WHERE id ?, (registration_id,) ).fetchone() if not reg or reg[status] 已退: raise ValueError(挂号记录不存在或已退) if reg[status] 已缴费: raise ValueError(已缴费的号不能直接退请走退费流程) conn.execute( UPDATE registrations SET status 已退 WHERE id ?, (registration_id,) )全部条件判断使用 SQL 占位符?而不是字符串拼接这是防止 SQL 注入的标准做法答辩的时候老师可能会专门问这一点。find_or_create_patient函数把“建档”和“查询”合并第一次挂号的患者自动建档第二次挂号直接复用这样能保证同一个人只有一条患者记录。register里限制同一医生同一工作日只能有一条在途挂号避免出现一人挂两个号的脏数据。事务提交的代码要单独写在调用方我一般会在主流程里用try/except包住最后统一commit出错时rollbackconn get_conn() try: pid find_or_create_patient(conn, 王小明, 男, 28, 110101199801011234, 13900000000) reg_id register(conn, pid, 1) pay(conn, reg_id) conn.commit() print(挂号缴费成功挂号单号, reg_id) except Exception: conn.rollback() raise finally: conn.close()这里conn.commit()之前的所有操作是原子的建档、挂号、缴费任何一个失败整条数据都不会落库。Python 定义函数时要注意参数顺序把连接对象放在第一个参数方便以后写单元测试时传入 mock 连接。3.3 前端接线让数据从点击流到数据库如果走 Flask 路线前端就是一个 HTML 表单加一段fetch请求。这里给出一个/api/register接口端到端示例把上一节的 service 函数暴露成 HTTP 接口。from flask import Flask, request, jsonify app Flask(__name__) app.post(/api/register) def api_register(): data request.get_json() name data.get(name) id_card data.get(id_card) doctor_id data.get(doctor_id) if not name or not id_card or not doctor_id: return jsonify({ok: False, msg: 缺少必填参数}), 400 conn get_conn() try: pid find_or_create_patient(conn, name, data.get(gender), data.get(age), id_card, data.get(phone)) reg_id register(conn, pid, doctor_id) conn.commit() return jsonify({ok: True, registration_id: reg_id}) except ValueError as e: conn.rollback() return jsonify({ok: False, msg: str(e)}), 400 finally: conn.close()这段路由把参数解析、业务校验、数据库操作分开接口只负责拿前端传过来的 JSON 结构数据真正的规则判断都在 service 层。用app.post是 Flask 2.0 以后支持的方式如果你用的是旧版 Flask可以改成app.route(/api/register, methods[POST])。前端页面只需要一个提交函数form idregisterForm input namename placeholder患者姓名 required input nameid_card placeholder身份证号 required select namedoctor_id option value1张医生内科/option option value2李医生外科/option /select button typesubmit挂号/button /form script document.getElementById(registerForm).addEventListener(submit, async (e) { e.preventDefault(); const f e.target; const body { name: f.name.value, id_card: f.id_card.value, doctor_id: Number(f.doctor_id.value) }; const res await fetch(/api/register, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(body) }); const data await res.json(); alert(data.ok ? 挂号成功单号 data.registration_id : data.msg); }); /script前端required只是做基础提醒真正的参数校验必须在后端再做一遍。原因很简单前端校验可以被绕过而后端校验才是数据能否落库的最后一道闸门。整套流程跑通后你会发现数据从表单到数据库只经过一个接口排查问题时只需要看网络请求和数据库两张表不用满项目找 bug。4. 小组作业避坑5 个让人翻车的问题与排查4.1 患者姓名写进 SQLite 变成乱码现象页面上输入中文姓名打开数据库看到???或者一串乱码控制台打印正常但写入后异常。原因Windows 下 Python 读取文件或控制台输入时使用了 GBK 编码而 SQLite 连接默认按 UTF-8 处理。最常见的是代码文件没有声明编码或者前端请求头里Content-Type没有指定application/json; charsetutf-8。解决所有.py文件保持 UTF-8 编码避免用记事本改代码Flask 接口在返回 JSON 时确保app.config[JSON_AS_ASCII] FalseFlask 2.2 之后用app.json.ensure_ascii False。数据库连接后可以执行一句PRAGMA encoding UTF-8;来确认编码策略但真正要排查的是写入链路前端的编码。4.2 双击运行报错找不到 his.db现象在 IDE 里运行一切正常但打包后或换一台电脑双击启动程序提示no such table或unable to open database file。原因代码里用了相对路径his.db这个路径是相对于当前工作目录的。双击启动时工作目录可能变成了压缩包解压目录的上一级甚至变成系统临时目录数据库文件自然找不到。解决不要用相对路径改为基于代码文件位置拼绝对路径。在db.py开头加一段from pathlib import Path BASE_DIR Path(__file__).resolve().parent DB_PATH str(BASE_DIR / his.db)这样无论从哪个目录启动数据库文件都会在db.py同级的目录下创建和读取。答辩前最好把整个项目复制到一个全新目录再运行一次验证路径脚本是否真的生效。4.3 退号后号源数量没恢复现象退掉一个未缴费的号重新查数据库发现registrations表里那条记录还在医生当天号源数也没有加回来后续再挂号直接提示“已有挂号记录”。原因业务逻辑写了UPDATE registrations SET status已退后没有调用commit()或者退号的异常分支里没有回滚。SQLite 默认是自动开启事务的Python 的sqlite3模块在连接关闭时会回滚未提交的内容结果就是你感觉操作成功了但数据实际没保存。解决所有写操作必须在同一个try里完成最后统一conn.commit()。退号的函数本身不要开新事务让外层调用者控制提交时机。正确做法是退号时先查询原挂号状态如果是“已缴费”直接抛异常只有“待缴费”才允许置为“已退”同时把号源数量加回这一步也要和外层的 commit 保持同一个事务。4.4 演示时输入非数字直接崩现象年龄输入框里填了“abc”或者留空点提交后程序抛出ValueError页面直接白屏。原因前端没有做输入校验后端也没有做类型转换保护。int(data.get(age))在空值或非数字时必然抛异常。解决前端加typenumber和min/max后端做二次校验把年龄转换包在try里失败则返回友好错误信息。更完整的做法是用一个统一的参数校验函数把 name、id_card、doctor_id、age 全部过一遍不符合要求的直接返回 400不进入业务逻辑。4.5 Python 环境问题命令行和 IDE 各讲各的现象VSCode 里运行正常终端里执行python main.py却提示找不到模块flask或者提示python不是内部或外部命令。原因IDE 配置的 Python 解释器和系统 PATH 里的 Python 不是同一个。pip 装到了 IDE 的虚拟环境里终端用的是系统 Python两边互不相通。数据库接口、Flask 这类第三方库最容易出现这种问题。解决先在终端里确认python --version再用pip install flask装到同一个环境。装完库以后重启 IDE 和终端再试一次不要边跑边装库。遇到缺包时不要盲目换解释器看清报错信息里显示的是哪个路径再决定装到哪边。小组里如果有成员电脑反复出问题直接建议他重新走一遍 Python 安装教程把环境变量配置和 pip 路径确认以后再开始写代码能省下大量无意义的联调时间。5. 答辩前怎么验证跑一条端到端业务闭环并自检系统能启动只是第一步能经得起老师按按钮才是关键。我习惯在交付前写一个冒烟测试脚本把完整的业务闭环用代码跑一遍。这个脚本不需要覆盖全部功能但必须覆盖“建档—挂号—缴费—退号—再挂号”这条主链路并且每一步都做断言。def smoke_test(): conn get_conn() try: pid find_or_create_patient(conn, 冒烟测试, 女, 30, TEST_ID_CARD_001, 13800000001) reg_id register(conn, pid, 1) assert reg_id 0 pay(conn, reg_id) status conn.execute( SELECT status FROM registrations WHERE id ?, (reg_id,) ).fetchone()[status] assert status 已缴费 try: refund(conn, reg_id) raise AssertionError(已缴费的号不应该能直接退) except ValueError: pass conn.rollback() print(冒烟测试通过患者建档、挂号、缴费、退号校验均正常) except Exception: conn.rollback() print(冒烟测试失败请查看上方异常信息) raise finally: conn.close() if __name__ __main__: smoke_test()这段脚本故意做了两件事第一步是跑通正常流程第二步是验证异常分支——已缴费的号必须不能退。这一步如果没做老师正好点到“退费”按钮系统就会暴露业务漏洞。测试通过后再打开界面手动操作一遍确认页面展示和数据库状态一致。如果还配置了 Excel 导出报表把患者列表、收费汇总分别导出一份看看格式和金额有没有错位。报表这个点很加分老师如果问“你的数据凭什么可信”直接把导出的表格摆出来就能说明问题。我自己的血泪经验是不要相信“刚才还能跑”。答辩前把所有功能按老师可能的操作顺序走一遍每次操作后刷新数据库看状态变化这个习惯救过我很多次。有一次就是漏了退号后号源恢复的检查差点在演示现场翻车。现在我把冒烟测试脚本写进了项目的README里告诉我带过的每一组先跑脚本再点界面。希望这个做法对你也有用帮你在小组作业里少踩几个坑。本文还有配套的精品资源点击获取