Java医院管理系统实战:JavaFX+文本文件存储与排序算法应用

发布时间:2026/9/14 6:29:12
Java医院管理系统实战:JavaFX+文本文件存储与排序算法应用 简介基于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 后第一眼不是服务端工程而是src与bin平铺的 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 TextRepositoryT { private final Path path; private final FunctionString, T lineMapper; public TextRepository(Path path, FunctionString, T lineMapper) { this.path path; this.lineMapper lineMapper; } public ListT 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负责把一行文本按分隔符拆开后构造成Registration、Prescription等模型。Files.lines会按平台的默认编码读跨 Windows 和其他系统容易出现乱码所以这里显式指定StandardCharsets.UTF_8后面冷启动时也要注意统一编码。2.3 退号时的一致性文件系统没有事务但要模拟事务数据库中可以用一条UPDATE完成退号文本文件做不到。常见做法是启动时把所有挂号流水加载到内存里的List修改对象后再整体写回文件。为了不让并发点击产生脏数据服务层方法会加synchronizedpublic 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 ListRegistration 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, MapMedicine, Integer items) { for (Map.EntryMedicine, 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 { ListPatient sort(ListPatient source, ComparatorPatient comparator); }4.2.1 冒泡排序实现冒泡排序的 Java 实现并不复杂但在医疗场景里必须注意不能直接改source列表否则会污染待诊队列。public class BubblePatientSorter implements PatientSorter { Override public ListPatient sort(ListPatient source, ComparatorPatient comparator) { ListPatient 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 ListPatient sort(ListPatient source, ComparatorPatient comparator) { ListPatient result new ArrayList(source); quickSort(result, 0, result.size() - 1, comparator); return result; } private void quickSort(ListPatient list, int left, int right, ComparatorPatient 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(ListPatient list, int left, int right, ComparatorPatient 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默认是最小堆队首是最小元素。要实现“紧急患者先看”需要传一个反向比较器让优先级数字大的反而排在前面PriorityQueuePatient 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); }PriorityQueue的offer和poll都是 O(log n)比每次重新排序快得多。分诊台把某位患者优先度改成10后重新offer进堆就能让他在下次poll时被取走。一个容易踩的坑是不要直接修改已入堆对象的priority字段而不重新入堆堆结构不会自动感知字段变化要么先remove再offer要么用带更新机制的自定义队列。4.4 链表维护优先度处理“加塞”与“取消”最大堆擅长取最值但不擅长“把某个患者从中间移给另一位医生”。双向链表适合这种频繁插入、删除的场景。LinkedList在 Java 里已经封装好了但核心逻辑要理解清楚public void upsertByPriority(LinkedListPatient list, Patient patient) { list.removeIf(p - p.getRegId().equals(patient.getRegId())); ListIteratorPatient 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只要controls和fxml两个模块就足够。若项目没有 FXML而是纯代码写界面可以去掉javafx.fxml。IDE 里跑更快但命令行方式能最快暴露缺包问题。5.3 冷启动常见的三类问题现象原因处理中文乱码文件编码与运行环境默认编码不一致代码统一用StandardCharsets.UTF_8JVM 加-Dfile.encodingUTF-8ClassNotFoundException: javafx未找到 JavaFX 模块检查--module-path路径确认lib下有javafx.controls.jarNumberFormatException文本行有空行或分隔符不匹配读取时trim()并过滤空行检查每一列是否与模型字段一一对应退号按钮无反应状态值不是WAIT打印当前Registration的状态字段确认FINISHED已被拒绝进入退号逻辑用-Dfile.encodingUTF-8启动后再观察RegistrationData.txt里退号前后状态位的变化就能判断整个门诊流程的状态传递是否符合预期。本文还有配套的精品资源点击获取