Java医院管理系统实战:JavaFX+文本文件存储与排序算法应用
2026/9/14 6:29:07 网站建设 项目流程

简介:基于Java的云医院管理系统(HIS)源码工程,面向计算机专业学习者、毕业设计学生及医疗信息化开发者,以医院门诊流程为主线,覆盖挂号退号、分诊叫号、医生诊断、开药收费、管理员后台等核心业务,并实现门诊病历首页、检查申请、成药处方、待诊/已诊患者列表及历史就诊记录等细节功能。压缩包共63个文件,包含18个java源码、20个class编译文件、8个fxml界面布局、2个jar库及CSS样式、文本数据和说明文档,整体仅1.43MB。源码中涉及病种树查询、冒泡排序、快速排序、最大堆以及链表实现患者优先度等数据结构应用,采用JavaFX结合FXML与CSS构建界面,分层结构清晰,适合用于课程设计、项目实训或HIS系统二次开发参考。目前已有55人学习浏览,目录结构规整,附有README和示例数据,便于快速导入与运行调试。

1. 把医院门诊搬进 Java 桌面程序

医院信息管理系统不一定非要微服务加数据库起步。这套基于 Java 的云医院管理系统,用 JavaFX 搭桌面端,用文本文件做持久化,把挂号、退号、分诊、医生诊断、开药、收款结算完整串了起来。初看数据存储比较原始,实际上把 Java 基础占得很全:集合框架、文件 IO、流式查询,以及手写冒泡排序、快速排序、最大堆和链表。适合两类人:拿医院管理系统做课程设计的学生,和想从零拆解 Java 业务代码、顺便补面试知识点的从业者。

2. 项目骨架、数据文件与领域模型

解压 ZIP 后,第一眼不是服务端工程,而是srcbin平铺的 JavaFX 项目。build.fxbuild是 e(fx)clipse 生成 JavaFX 工程时留下的构建描述文件,说明主程序入口在src/application包下;bin是编译输出,images放界面用图,README.md是这个项目最好的“代码注释”。

2.1 从 build.fxbuild 看启动入口和包结构

资源包里的结构大致是这样:

云医院管理系统/ ├── build.fxbuild ├── README.md ├── images/ # 界面图标、科室图片 ├── src/application/ # JavaFX 主程序、控制器、模型、服务 ├── bin/ # 编译输出目录 ├── UserData.txt # 用户账号 ├── Department.txt # 科室与医生 ├── RegistrationData.txt # 挂号流水 ├── MedicineList.txt # 药品目录 ├── Prescription.txt # 处方记录 └── PrescriptionModel.txt # 处方明细/模板

application包是 JavaFX 生成工程时的默认根包,Main类通常负责加载 FXML、启动窗口。中间层不要堆在一处,我一般会按model / service / controller拆:模型对应文本文件里的每一条记录,服务层负责挂号、分诊、开药的业务规则,控制器只做界面事件转发。这样后面加菜单、加列表都更直接。

2.2 文本文件即“数据库”:每个 txt 对应什么业务

这套系统没有接 MySQL,而是用六个文本文件保存核心数据。把文件到业务的映射先理清,比直接读代码省力:

数据文件业务对象在门诊流程中的作用
UserData.txt用户账号管理员和医生的登录、权限控制
Department.txt科室与医生挂号时选择科室、医生
RegistrationData.txt挂号流水挂号、退号、分诊状态变化
MedicineList.txt药品目录开药时检索药品、读取价格
Prescription.txt处方主表一次开药的汇总记录
PrescriptionModel.txt处方明细/模板记录每种药品的数量和用法

文本文件没有主键、外键和事务,所以代码里通常会写一个通用读取类,把这些文件按行转成对象:

public class TextRepository<T> { private final Path path; private final Function<String, T> lineMapper; public TextRepository(Path path, Function<String, T> lineMapper) { this.path = path; this.lineMapper = lineMapper; } public List<T> loadAll() { try { return Files.lines(path, StandardCharsets.UTF_8) .map(String::trim) .filter(line -> !line.isEmpty()) .map(lineMapper) .collect(Collectors.toList()); } catch (IOException e) { throw new UncheckedIOException("读取 " + path + " 失败", e); } } }

TextRepository把“读文件并转对象”的动作固定下来。构造时传入两个参数:path指向某个 txt 文件,lineMapper负责把一行文本按分隔符拆开后构造成RegistrationPrescription等模型。Files.lines会按平台的默认编码读,跨 Windows 和其他系统容易出现乱码,所以这里显式指定StandardCharsets.UTF_8,后面冷启动时也要注意统一编码。

2.3 退号时的一致性:文件系统没有事务,但要模拟事务

数据库中可以用一条UPDATE完成退号,文本文件做不到。常见做法是启动时把所有挂号流水加载到内存里的List,修改对象后再整体写回文件。为了不让并发点击产生脏数据,服务层方法会加synchronized

public synchronized void cancelRegistration(String regId) { Registration r = findById(regId) .orElseThrow(() -> new IllegalArgumentException("挂号单不存在")); if (!"WAIT".equals(r.getStatus())) { throw new IllegalStateException("当前状态不允许退号"); } r.setStatus("CANCELED"); registrationRepository.saveAll(allRegistrations); }

这段代码的关键不是文件写入,而是状态校验:只有WAIT状态的挂号单才能退号。如果已经是FINISHED,说明医生已经诊毕,应该走退费流程而不是简单退号。saveAll的代价是每次都重写整个文件,数据量小的时候完全够用,这也是这类教学项目选择文本文件的原因:每个步骤都能用记事本打开验证。

3. 分诊台到医生站:挂号、待诊、诊断、收费的核心链路

门诊模块是整个系统的业务主线。正确理解这条链路,比单独看某个界面更重要:挂号产生挂号单,分诊把患者加入待诊队列,医生站从待诊列表选取患者,诊断后开出处方和检查申请,最后收款并改变患者状态。

3.1 挂号与分诊:把一条流水变成待诊患者

挂号动作会产生一条RegistrationData,同时把患者放入分诊队列。一个简化但可运行的实现是:

public Registration registerAndTriage(Patient patient, Department dept, Doctor doctor) { Registration reg = new Registration( UUID.randomUUID().toString().substring(0, 8), patient.getId(), dept.getId(), doctor.getId(), "WAIT", LocalDateTime.now()); registrationRepository.append(reg); triageQueue.add(reg); return reg; }

挂号单号用UUID前 8 位生成,只有冲突概率足够低,才能避免主键重复;状态用WAIT表示待诊。triageQueue.add(reg)是关键,它同时把挂号单交给分诊队列,这样分诊界面才能立刻看到新患者。后面分诊台设置优先度,本质上是修改Registration上的priority字段,再刷新队列排序。

分诊功能里还有一个容易忽略的点:患者加入待诊列表后,医生站的待诊列表要能实时刷新。如果界面刷新按钮只是重新查一次RegistrationData.txt,就看不到队列内存中还没落盘的临时状态变化,所以刷新逻辑要同时读内存队列和文件快照。

3.2 医生站三张列表:待诊、已诊与未诊的状态过滤

门诊医生站通常同时展示三类患者:待诊、已诊、未诊。它们的区别其实就是状态条件不同:

列表过滤条件界面用途
待诊列表WAIT当前等待叫号的患者
已诊列表FINISHED本次就诊已完成,可查看历史
未诊列表CANCELED/NO_SHOW退号、过号或分诊撤销的记录

实现上不需要建三个查询方法,一个带状态参数的方法就够了:

public List<Registration> listForDoctor(String doctorId, String status) { return registrationRepository.findAll().stream() .filter(r -> doctorId.equals(r.getDoctorId())) .filter(r -> status.equals(r.getStatus())) .sorted(Comparator.comparing(Registration::getPriority).reversed() .thenComparing(Registration::getRegTime)) .collect(Collectors.toList()); }

参数一doctorId保证每个医生只看到自己名下的患者,参数二status决定当前显示哪个列表。排序部分对priority降序,再按挂号时间升序,这个顺序会在第四章展开。待诊列表的患者历史就诊记录,可以用patientId再查一次FINISHED状态的挂号流水,不需要单独建病历表。

3.3 诊断、成药处方、检查申请与诊毕结算

医生诊断后,最关键的动作是开成药处方。处方至少包含两层数据:处方主表记录这次开了多少药、总计多少钱,处方明细记录每一种药的数量和用法。开药需要同时校验库存:

public Prescription prescribe(Registration reg, Map<Medicine, Integer> items) { for (Map.Entry<Medicine, Integer> e : items.entrySet()) { if (medicineStock.get(e.getKey().getId()) < e.getValue()) { throw new InsufficientStockException(e.getKey().getName()); } } double total = items.entrySet().stream() .mapToDouble(e -> e.getKey().getPrice() * e.getValue()) .sum(); Prescription presc = new Prescription(reg.getId(), total, LocalDateTime.now()); prescriptionRepository.append(presc); items.forEach((medicine, qty) -> { medicineStock.decrease(medicine.getId(), qty); prescriptionItemRepository.append( new PrescriptionItem(presc.getId(), medicine.getId(), qty)); }); return presc; }

方法先统一检查库存,再计算处方总价,最后写处方主表和明细表。先校验后写入,是为了避免写入一半才发现库存不足。prescriptionItemRepository.append每次追加一条明细,明细与主表通过prescId关联。实际项目里诊毕按钮通常会在收款成功后才把患者状态改成FINISHED,否则退费时要同时处理药品库存回滚,文本文件场景下会比较麻烦。

至于检查申请,可以在处方开出后单独生成一条检查记录,界面上的“病种树”用层级结构维护诊断名称,点中叶子节点后把诊断结果带入病历主诉区域。

4. 患者排序与优先度管理:冒泡、快排、最大堆和链表

这条待诊队列看似简单,但涉及多个 Java 数据结构:普通列表按时间排序、紧急患者插队、动态调整位置。一个待诊患者数量不多的桌面系统,为什么要把冒泡排序、快速排序、最大堆和链表全用上?因为每种结构解决的问题不同。

4.1 一条待诊列表,为什么需要四种算法

场景数据结构/算法复杂度项目落点
科室患者少,排序要直观冒泡排序O(n²)演示排序过程、小列表稳定输出
大量挂号记录按时间排序快速排序O(n log n) 平均管理员查询历史就诊记录
危重患者随时插队优先最大堆插入 O(log n),取最大 O(log n)分诊台设置高优先度后的叫号逻辑
中间插入、前后移动队伍双向链表删除节点 O(n)手动调整待诊顺序

项目后来即使接数据库,也未必需要手写这些算法,但在这个源码场景里,它们承担了“让排序看得见”的教学作用,也是 Java 面试里最常被问到的数据结构。

4.2 手写 PatientSorter 接口与两种排序

为了让医生站不需要关心排序细节,可以抽一个PatientSorter接口。传入患者列表和一个比较器,返回排序后的新列表,不修改原集合。

public interface PatientSorter { List<Patient> sort(List<Patient> source, Comparator<Patient> comparator); }
4.2.1 冒泡排序实现

冒泡排序的 Java 实现并不复杂,但在医疗场景里必须注意:不能直接改source列表,否则会污染待诊队列。

public class BubblePatientSorter implements PatientSorter { @Override public List<Patient> sort(List<Patient> source, Comparator<Patient> comparator) { List<Patient> result = new ArrayList<>(source); int n = result.size(); for (int i = 0; i < n - 1; i++) { boolean swapped = false; for (int j = 0; j < n - 1 - i; j++) { if (comparator.compare(result.get(j), result.get(j + 1)) > 0) { Collections.swap(result, j, j + 1); swapped = true; } } if (!swapped) { return result; } } return result; } }

比较器由调用方决定,可以按挂号时间升序,也可以按优先度降序。swapped标识用于提前终止:这一轮没有任何交换,说明列表已经有序。小规模数据下虽然复杂度是 O(n²),但代码直观,适合放在分诊台少量患者列表上。

4.2.2 快速排序实现

快速排序教科书写法很多,这套源码场景下采用“选最右元素作为基准值、原地分区”的写法最稳妥:

public class QuickPatientSorter implements PatientSorter { @Override public List<Patient> sort(List<Patient> source, Comparator<Patient> comparator) { List<Patient> result = new ArrayList<>(source); quickSort(result, 0, result.size() - 1, comparator); return result; } private void quickSort(List<Patient> list, int left, int right, Comparator<Patient> comparator) { if (left >= right) { return; } int pivot = partition(list, left, right, comparator); quickSort(list, left, pivot - 1, comparator); quickSort(list, pivot + 1, right, comparator); } private int partition(List<Patient> list, int left, int right, Comparator<Patient> comparator) { Patient pivot = list.get(right); int i = left - 1; for (int j = left; j < right; j++) { if (comparator.compare(list.get(j), pivot) <= 0) { i++; Collections.swap(list, i, j); } } Collections.swap(list, i + 1, right); return i + 1; } }

quickSort的区间参数是闭区间,所以递归调用时左段用pivot - 1,右段用pivot + 1。基准值取最右元素后,partition把所有小于等于基准值的患者交换到左边,最后把基准值放到正确位置。这里用<= 0保证相同优先级时原有相对顺序不被破坏。

4.3 最大堆:让危重患者先见医生

Java 的PriorityQueue默认是最小堆,队首是最小元素。要实现“紧急患者先看”,需要传一个反向比较器,让优先级数字大的反而排在前面:

PriorityQueue<Patient> maxHeap = new PriorityQueue<>( Comparator.comparingInt(Patient::getPriority).reversed() .thenComparing(Patient::getRegTime) ); for (Patient p : waitingPatients) { maxHeap.offer(p); } while (!maxHeap.isEmpty()) { Patient next = maxHeap.poll(); doctorDesk.show(next); }

PriorityQueueofferpoll都是 O(log n),比每次重新排序快得多。分诊台把某位患者优先度改成10后,重新offer进堆就能让他在下次poll时被取走。一个容易踩的坑是:不要直接修改已入堆对象的priority字段而不重新入堆,堆结构不会自动感知字段变化,要么先removeoffer,要么用带更新机制的自定义队列。

4.4 链表维护优先度:处理“加塞”与“取消”

最大堆擅长取最值,但不擅长“把某个患者从中间移给另一位医生”。双向链表适合这种频繁插入、删除的场景。LinkedList在 Java 里已经封装好了,但核心逻辑要理解清楚:

public void upsertByPriority(LinkedList<Patient> list, Patient patient) { list.removeIf(p -> p.getRegId().equals(patient.getRegId())); ListIterator<Patient> it = list.listIterator(); while (it.hasNext()) { if (patient.getPriority() > it.next().getPriority()) { it.previous(); it.add(patient); return; } } list.addLast(patient); }

先按挂号单号删除已有记录,再按优先级从高到低找到第一个比自己低的位置插入。ListIterator.previous()因为it.next()已经越过了目标节点,所以回退一位再add,新节点会落在当前位置之前。这个写法比for循环加list.add(index)更安全,避免连续插入时索引漂移的问题。

5. 药品管理、冷启动与文本文件排错

药品管理不只是增删改查。加减库存要和处方联动,而文本文件没法保证原子性,所以代码里要把“先检查、再扣减、后写明细”的顺序固定住。

5.1 药品库存与处方联动

开处方时先校验库存,计算总价后写处方主表,再逐条扣减库存并写明细。退药时反向操作,按原数量加回库存,同时把明细状态置为RETURNED。如果一个处方开了多种药,其中一种退药失败,最简单的方式是把整张处方视为已失效,而不是只改某一行。

5.2 拿到源码后怎么把它跑起来

JavaFX 项目直接从命令行走会有模块化问题,先把 FXML 和图片资源放到src下,再用模块参数启动:

cd 云医院管理系统 javac -encoding UTF-8 -d out \ --module-path /path/to/javafx/lib \ --add-modules javafx.controls,javafx.fxml \ $(find src -name "*.java") java --module-path /path/to/javafx/lib \ --add-modules javafx.controls,javafx.fxml \ -cp out application.Main

--module-path指向本机 JavaFX 的lib目录,--add-modules只要controlsfxml两个模块就足够。若项目没有 FXML,而是纯代码写界面,可以去掉javafx.fxml。IDE 里跑更快,但命令行方式能最快暴露缺包问题。

5.3 冷启动常见的三类问题

现象原因处理
中文乱码文件编码与运行环境默认编码不一致代码统一用StandardCharsets.UTF_8,JVM 加-Dfile.encoding=UTF-8
ClassNotFoundException: javafx未找到 JavaFX 模块检查--module-path路径,确认lib下有javafx.controls.jar
NumberFormatException文本行有空行或分隔符不匹配读取时trim()并过滤空行,检查每一列是否与模型字段一一对应
退号按钮无反应状态值不是WAIT打印当前Registration的状态字段,确认FINISHED已被拒绝进入退号逻辑

-Dfile.encoding=UTF-8启动后,再观察RegistrationData.txt里退号前后状态位的变化,就能判断整个门诊流程的状态传递是否符合预期。

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

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

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

立即咨询