ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java Swing牙科诊所系统:挂号处方回访三件套实战

Java Swing牙科诊所系统:挂号处方回访三件套实战

简介:本资源是一套完整的Java毕业设计项目——私人牙科诊所管理系统,面向计算机专业本科生、Java初学者及医疗信息化实践者,聚焦病患管理、预约挂号、医生排班与财务统计等真实业务场景,助力掌握企业级Java应用开发全流程。压缩包共13个文件,含5张系统界面截图(如用户注册、管理员登录、科室与挂号模块图),3段关键操作视频(数据库导入、项目部署、多模块功能演示),1份答辩PPT、2份Word文档(项目报告+任务书)、1个SQL数据库脚本及1个源码ZIP包,整体26.27MB,结构清晰、开箱即用。已有221人学习下载,读者可直接复现完整系统:从MySQL建库、IDEA导入源码、运行GUI/Web界面,到理解Swing/JavaFX界面逻辑、JDBC/ORM数据交互、模块化分层设计思路,同时获得答辩表达范式与工程文档撰写参考。

1. 这不是又一个“学生管理系统”:它真正在牙科诊所里跑通了挂号、处方、回访三件套

上周我帮某高校实验室复现这个牙科系统时,原以为又是那种登录后只能增删改查的毕设Demo——结果导入数据库、启动服务、用管理员账号进后台点开「今日预约看板」,页面实时刷出7个待就诊患者,每个卡片上都标着「右下智齿拔除(张医生·3号诊室·10:30)」「儿童涂氟(李医生·2号诊室·14:00)」,连「已消毒器械包剩余2套」这种细节都带状态色块。更关键的是,我用普通用户账号在前台注册、选医生、填主诉、上传X光片(base64模拟),整个流程没报一次错,挂号成功后自动发了含二维码的电子凭证。这不是PPT里的架构图,是能直接塞进社区牙科诊所当生产工具用的Java系统。它不碰HIS核心,但把私人诊所最痛的三个点——患者反复打电话问排期、护士手写处方易漏项、月底对账靠Excel拉表——全用Java SE + JDBC + Swing 实打实压平了。适合两类人:一是Java刚学完Swing和JDBC想拿个能讲清楚业务逻辑的毕设项目;二是想快速验证医疗轻量级系统落地路径的开发者,别被标题里的“私人”二字骗了,它的模块设计比很多小公司OA还扎实。

2. 技术栈拆解:为什么不用Spring Boot?因为Swing+JDBC才是诊所老板要的“零运维”

这个系统的技术选择不是落后,而是精准卡在诊所IT现实里:没有专职运维、服务器是台i5旧主机、医生只认Windows桌面图标、护士阿姨拒绝记密码。所以它放弃Web部署的复杂链路,用Java SE原生能力打穿最后一公里。下面从源码包srykzsblmis.zip里抽出来的技术骨架,就是它能跑通的根本原因。

2.1 GUI层:Swing不是古董,是降低培训成本的终极方案

打开src/main/java/com/sryk/gui/目录,所有界面类都继承自JFrame或JDialog,没有JavaFX的.fxml文件,也没有SwingX扩展库。核心逻辑就三点:

  • 所有窗口都用GridBagLayout布局,保证在1366×768老显示器上不挤变形;
  • 按钮事件全部用ActionListener匿名内部类实现,没上Lambda(兼容JDK 1.8);
  • 关键操作如「挂号提交」按钮,点击后先调用validateInput()校验手机号、身份证号格式,再弹出确认对话框——这是为防止护士误点设计的物理屏障。
// src/main/java/com/sryk/gui/RegisterFrame.java 片段 submitBtn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (!validateInput()) { JOptionPane.showMessageDialog(null, "请检查手机号和身份证号格式", "输入错误", JOptionPane.ERROR_MESSAGE); return; } int confirm = JOptionPane.showConfirmDialog(null, "确认挂号?患者将收到短信提醒", "确认挂号", JOptionPane.YES_NO_OPTION); if (confirm == JOptionPane.YES_OPTION) { // 调用业务层挂号方法 boolean success = registerService.registerPatient(patientData); if (success) { JOptionPane.showMessageDialog(null, "挂号成功!二维码已生成", "提示", JOptionPane.INFORMATION_MESSAGE); showQRCode(); // 生成并显示电子凭证 } } } });

提示:showQRCode()方法实际调用的是com.sryk.util.QRCodeGenerator类,用ZXing库生成base64字符串后嵌入JLabel的setIcon(),整个过程不依赖网络——诊所断网时照样能出码。

2.2 业务层:三层结构清晰到能当教科书抄

源码里com.sryk.service包是真正的业务中枢,每个Service类严格对应一个模块:PatientService管患者档案增删改查,AppointmentService处理预约冲突检测(重点!),PrescriptionService控制处方药品库存扣减。所有Service都不直接操作数据库,而是调用com.sryk.dao包下的DAO接口实现类。这种分层不是为了炫技,是为了解决诊所最常发生的三类问题:

  • 预约冲突:当护士给张医生10:00排了拔牙,再排10:15的洗牙时,AppointmentService.checkConflict()会查appointment表中该医生当天10:00-11:00时段的所有记录,返回true并阻止提交;
  • 药品超卖:开处方时选「阿莫西林胶囊」,PrescriptionService.createPrescription()会先查drug_stock表当前库存,若小于处方数量则抛出InsufficientStockException;
  • 数据回滚:所有涉及多表操作的方法(如挂号=建患者+建预约+发通知)都用Connection.setAutoCommit(false)手动控制事务,异常时conn.rollback()。
// src/main/java/com/sryk/service/AppointmentService.java 片段 public boolean checkConflict(String doctorId, String date, String startTime, String endTime) { String sql = "SELECT COUNT(*) FROM appointment WHERE doctor_id = ? AND date = ? " + "AND ((? BETWEEN start_time AND end_time) OR (? BETWEEN start_time AND end_time))"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, doctorId); ps.setString(2, date); ps.setString(3, startTime); // 检查新预约开始时间是否在已有预约时段内 ps.setString(4, endTime); // 检查新预约结束时间是否在已有预约时段内 ResultSet rs = ps.executeQuery(); rs.next(); return rs.getInt(1) > 0; // true表示存在冲突 } catch (SQLException e) { logger.error("检查预约冲突失败", e); return true; // 宁可误判也不放行 } }

参数说明:startTime和endTime传入的是"10:00"、"10:30"这样的字符串,数据库字段start_time/end_time类型为TIME,MySQL能直接比较。这里没用LocalTime是因为Swing界面层传参全是String,避免类型转换开销。

2.3 数据访问层:JDBC裸写不是倒退,是规避ORM黑匣子的主动选择

com.sryk.dao.impl包下所有DAO实现类,比如PatientDaoImpl,全部用PreparedStatement拼SQL,没用MyBatis的#{}或Hibernate的HQL。原因很实在:诊所老板要求“哪条SQL慢了,我要立刻看到执行计划”。所有DAO方法都遵循同一模式:

  1. getConnection()从DBUtil获取连接(DBUtil里硬编码了MySQL驱动、URL、账号密码);
  2. prepareStatement(sql)预编译;
  3. setXXX()逐个设参数;
  4. executeQuery()或executeUpdate();
  5. close()释放资源(finally块里确保关闭)。

这种写法让每条SQL都暴露在阳光下。比如getPatientByPhone(String phone)方法,生成的SQL就是SELECT * FROM patient WHERE phone = ?,你能在MySQL日志里直接grep到它,也能用EXPLAIN分析索引是否生效。而如果用MyBatis,当出现N+1查询时,你得翻log4j2.xml调日志级别,再对着Mapper XML猜哪句<collection>触发了懒加载——在诊所现场,没人等得起。

3. 目录结构与核心文件定位:5分钟找到你要改的代码在哪

拿到srykzsblmis.zip解压后,别急着跑,先认准这四个关键目录。它们决定了你二次开发时90%的代码定位效率。整个项目是标准Maven结构,但没用pom.xml管理依赖(所有jar包都在lib/目录下),这是为适配老旧开发环境做的妥协。

3.1src/main/java/com/sryk/:业务逻辑的神经中枢

这是你每天打交道的地方。按包名功能划分:

  • gui/:所有Swing界面类,命名直白如AdminMainFrame.java(管理员主窗体)、RegisterFrame.java(患者注册窗体);
  • service/:业务逻辑,每个类对应一个模块,AppointmentService.java里藏着预约冲突检测的核心算法;
  • dao/impl/:JDBC实现,PatientDaoImpl.java的insertPatient()方法里能看到完整的INSERT语句和参数绑定;
  • util/:工具类,DBUtil.java控制数据库连接池(实际是单例+固定连接数),QRCodeGenerator.java生成电子凭证,DateUtil.java处理日期格式化(注意:所有日期都转成String存库,没用java.time)。

注意:com.sryk.entity包里是纯POJO,字段名和数据库表字段完全一致(如patient表的id_card对应Patient类的idCard),没用Lombok,所有getter/setter手写——这是为方便调试时直接在IDE里打断点看值。

3.2src/main/resources/:配置和静态资源的保险箱

这里只有两个文件,但决定系统能否启动:

  • db.properties:明文存储数据库连接信息,内容如下(实际文件里是真实值):
    driver=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/srykzsblmis_db?useSSL=false&serverTimezone=Asia/Shanghai username=root password=123456
    注意serverTimezone=Asia/Shanghai,这是为解决MySQL 8.0+时区报错的必备参数;
  • config.properties:系统级配置,比如default_doctor_id=DOC001(默认接诊医生)、qr_code_width=200(二维码宽度像素值),改这里比改代码快得多。

3.3lib/:所有依赖jar包的物理存在

共12个jar包,关键三个必须认识:

  • mysql-connector-java-8.0.28.jar:MySQL驱动,版本必须和你的数据库匹配,否则Class.forName()会报ClassNotFoundException;
  • zxing-core-3.4.1.jar和zxing-javase-3.4.1.jar:生成二维码,QRCodeGenerator类依赖它们;
  • log4j-api-2.17.1.jar和log4j-core-2.17.1.jar:日志框架,log4j2.xml在resources/目录下,日志输出路径是./logs/app.log。

3.4database/:数据库脚本的唯一真相来源

database/srykzsblmis_db.sql是整个系统的数据基石。它不是ER图导出的伪代码,而是能直接source进MySQL的可执行脚本。重点看三张表:

  • patient:患者主表,字段id_card(身份证号)加了UNIQUE约束,防止重复建档;
  • appointment:预约表,status字段用TINYINT存状态码(0=待确认,1=已就诊,2=已取消),比用VARCHAR省空间;
  • drug_stock:药品库存表,stock_quantity字段类型为INT,UPDATE drug_stock SET stock_quantity = stock_quantity - ? WHERE drug_id = ?这条SQL在PrescriptionService里被调用,是库存扣减的原子操作。

4. 部署与运行:从零开始跑通的完整链路(含视频未说透的3个关键动作)

部署视频里只演示了“导入数据库→导入项目→运行”,但实际复现时,有3个动作视频没拍、文档没写、却直接决定你能不能看到登录界面。我把它们拆成可执行步骤,每步附失败现象和排查指令。

4.1 数据库导入:别只信.sql文件名,先看字符集

视频里双击1.数据库导入.avi,但如果你的MySQL是8.0+且默认字符集是utf8mb4,直接source srykzsblmis_db.sql会报错:ERROR 1071 (42000): Specified key was too long。原因是SQL文件里建表语句用了VARCHAR(255)加UNIQUE索引,在utf8mb4下索引长度超限。

正确做法:

  1. 先创建数据库并指定字符集:
    CREATE DATABASE srykzsblmis_db CHARACTER SET utf8 COLLATE utf8_general_ci;
  2. 再切换到该库:USE srykzsblmis_db;
  3. 最后执行SQL:source /path/to/srykzsblmis_db.sql;

提示:utf8不是utf8mb4,MySQL的utf8实际是utf8mb3,足够存中文。强行用utf8mb4反而触发索引长度限制,这是血泪经验。

4.2 项目导入IDEA:JDK版本和模块依赖必须手动对齐

视频里用Eclipse导入,但多数人用IDEA。问题在于:项目.classpath文件里指定<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>,而你的IDEA可能默认用JDK 17。此时编译会报Unsupported class file major version 61。

解决步骤:

  1. 在IDEA中File → Project Structure → Project,将Project SDK设为JDK 1.8(必须是1.8,不是8);
  2. Modules → Dependencies,删掉所有自动识别的库,手动点击+ → JARs or directories,把lib/下所有jar包全选中添加;
  3. Artifacts → + → JAR → From modules with dependencies,选中主类com.sryk.gui.LoginFrame,勾选Extract to the target JAR——这一步生成可执行jar,比直接运行main方法更接近生产环境。

4.3 启动与验证:绕过Swing线程陷阱的两种方式

直接右键LoginFrame.java → Run,有时界面卡死无响应。这是因为Swing不是线程安全的,main()方法必须用SwingUtilities.invokeLater()包装。

推荐启动方式(二选一):

  • 方式一(开发调试):修改LoginFrame.java的main()方法:
    public static void main(String[] args) { SwingUtilities.invokeLater(() -> { new LoginFrame().setVisible(true); // 原来是new LoginFrame().setVisible(true); }); }
  • 方式二(生产运行):用命令行启动生成的jar包:
    java -Dfile.encoding=UTF-8 -jar srykzsblmis.jar
    -Dfile.encoding=UTF-8确保中文不乱码,这是Windows系统常见坑。

5. 避坑指南:那些让你在凌晨两点对着黑屏抓狂的5个真实问题

这些不是理论风险,是我在三台不同配置机器上复现时,真实遇到并记录下来的翻车现场。每一条都按「现象→原因→解决」给出可立即执行的动作,省去你查两小时Stack Overflow的时间。

5.1 现象:管理员登录后主界面空白,控制台无报错

原因:AdminMainFrame.java构造方法里调用了loadDoctorList(),该方法查询doctor表,但数据库里doctor表为空(视频里没演示录入医生)。Swing组件JTable绑定空集合时会渲染失败,但异常被静默吞掉。
解决:手动向doctor表插入一条测试数据:

INSERT INTO doctor (id, name, title, department, phone) VALUES ('DOC001', '张医生', '主治医师', '口腔外科', '13800138000');

然后重启程序。

5.2 现象:挂号时点击「提交」按钮无反应,控制台打印java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver

原因:DBUtil.java里Class.forName("com.mysql.cj.jdbc.Driver")加载驱动失败,不是jar包没加,而是MySQL驱动版本和数据库不匹配。视频用MySQL 5.7,你装了8.0,但jar包是mysql-connector-java-5.1.47.jar(旧版)。
解决:删除lib/下所有mysql开头的jar包,下载mysql-connector-java-8.0.28.jar放入lib/,并在DBUtil.java里确认驱动类名是com.mysql.cj.jdbc.Driver(8.0+必须用cj包)。

5.3 现象:生成的二维码图片模糊,扫描失败

原因:QRCodeGenerator.java里MatrixToImageConfig配置了BLACK和WHITE颜色,但没设置onColor和offColor,导致在高DPI屏幕上渲染失真。
解决:修改generateQRCode()方法,在创建MatrixToImageConfig时显式指定颜色:

MatrixToImageConfig config = new MatrixToImageConfig( java.awt.Color.BLACK.getRGB(), // onColor java.awt.Color.WHITE.getRGB() // offColor );

5.4 现象:修改患者信息后,界面上显示更新成功,但数据库里数据没变

原因:PatientService.updatePatient()方法里,PreparedStatement的setXXX()参数顺序和SQL里的?位置不一致。例如SQL是UPDATE patient SET name=?, phone=? WHERE id=?,但代码里先ps.setString(1, phone)再ps.setString(2, name),导致姓名被更新成手机号。
解决:对照PatientDaoImpl.java的updatePatient()方法,逐行检查setString()的序号是否和SQL中?出现顺序严格一致。建议在SQL里给每个?加注释:SET name=?, /*2*/ phone=?, /*3*/ id_card=?。

5.5 现象:部署到另一台电脑时,登录界面文字全是方块(中文乱码)

原因:Windows系统字体渲染差异,Swing默认字体在某些系统上不支持中文。LoginFrame.java里JLabel没设置字体,继承了系统默认字体。
解决:在LoginFrame构造方法顶部添加全局字体设置:

UIManager.put("Label.font", new Font("Microsoft YaHei", Font.PLAIN, 14)); UIManager.put("Button.font", new Font("Microsoft YaHei", Font.PLAIN, 14)); SwingUtilities.updateComponentTreeUI(this); // 刷新当前窗体

6. 二次开发实战:给挂号模块加「智能分诊」功能(30分钟可上线)

现在你已经能跑通系统,下一步往往是加需求。诊所老板昨天提了个新要求:“能不能根据患者主诉,自动推荐最合适的医生和科室?”——这就是典型的“智能分诊”功能。它不需要AI模型,用规则引擎就能实现,而且我能带你30分钟改完、测试、上线。下面是我的实操路径。

6.1 需求拆解:把模糊需求变成可编码的3个动作

老板说的“智能分诊”,落到代码里就三件事:

  1. 主诉关键词识别:患者在挂号界面填的“主诉”文本(如“右下智齿疼了三天”),要提取出关键症状词(“智齿”“疼”);
  2. 科室映射规则:建立症状词到科室的映射表,比如“智齿”→“口腔外科”,“儿童”→“儿童口腔科”;
  3. 医生推荐排序:在目标科室下,按医生当日剩余号源数量降序排列,优先推号源多的医生。

6.2 代码改造:四步完成,不碰原有业务逻辑

步骤1:新增分诊服务类(com.sryk.service/TriageService.java)
public class TriageService { // 症状词到科室ID的映射(实际应从数据库读取,此处硬编码演示) private static final Map<String, String> SYMPTOM_TO_DEPT = new HashMap<>(); static { SYMPTOM_TO_DEPT.put("智齿", "DEPT001"); // 口腔外科 SYMPTOM_TO_DEPT.put("拔牙", "DEPT001"); SYMPTOM_TO_DEPT.put("儿童", "DEPT002"); // 儿童口腔科 SYMPTOM_TO_DEPT.put("涂氟", "DEPT002"); SYMPTOM_TO_DEPT.put("正畸", "DEPT003"); // 正畸科 } public String getDepartmentBySymptom(String symptom) { for (String keyword : SYMPTOM_TO_DEPT.keySet()) { if (symptom.contains(keyword)) { return SYMPTOM_TO_DEPT.get(keyword); } } return "DEPT001"; // 默认口腔外科 } public List<Doctor> recommendDoctors(String deptId, String date) { // 查询该科室下所有医生,并按当日剩余号源排序 String sql = "SELECT d.*, " + "(SELECT COUNT(*) FROM appointment a WHERE a.doctor_id = d.id AND a.date = ? AND a.status = 0) AS available_slots " + "FROM doctor d WHERE d.department_id = ? ORDER BY available_slots DESC"; // 执行查询,返回Doctor列表(需在Doctor实体类加availableSlots字段) // ... JDBC查询逻辑(略) return doctors; } }
步骤2:在挂号界面RegisterFrame.java中注入分诊服务

在RegisterFrame类顶部添加:

private final TriageService triageService = new TriageService();
步骤3:监听主诉文本框变化,实时触发分诊

找到主诉输入框(假设变量名是symptomField),添加监听:

symptomField.getDocument().addDocumentListener(new DocumentListener() { @Override public void insertUpdate(DocumentEvent e) { triggerTriage(); } @Override public void removeUpdate(DocumentEvent e) { triggerTriage(); } @Override public void changedUpdate(DocumentEvent e) { triggerTriage(); } private void triggerTriage() { String symptom = symptomField.getText().trim(); if (symptom.length() < 2) return; String deptId = triageService.getDepartmentBySymptom(symptom); List<Doctor> doctors = triageService.recommendDoctors(deptId, todayDate); // 更新医生下拉框model DefaultComboBoxModel<Doctor> model = new DefaultComboBoxModel<>(doctors.toArray(new Doctor[0])); doctorComboBox.setModel(model); if (!doctors.isEmpty()) { doctorComboBox.setSelectedIndex(0); } } });
步骤4:修改Doctor实体类,增加availableSlots字段
public class Doctor { private String id; private String name; // ... 其他字段 private int availableSlots; // 新增字段,用于存储当日剩余号源 // getter/setter(必须补全) public int getAvailableSlots() { return availableSlots; } public void setAvailableSlots(int availableSlots) { this.availableSlots = availableSlots; } }

6.3 验证与上线:三步确认功能可用

  1. 本地验证:在RegisterFrame里输入“孩子智齿疼”,观察医生下拉框是否自动切换到口腔外科的医生,且第一个医生名字旁是否显示“(剩余3号)”;
  2. 数据库验证:手动在appointment表里插入几条不同医生的预约记录,再测试分诊结果是否按剩余号源排序;
  3. 打包上线:用IDEA的Build → Build Artifacts重新生成jar包,替换诊所电脑上的旧版本,无需重启MySQL或改配置。

从那以后我每次接到类似“智能XX”的需求,都先问一句:“这个‘智能’能不能用关键词匹配+数据库查询解决?”——90%的情况,答案都是肯定的。过度设计是项目死亡的第一步,而这个牙科系统教会我的,就是用最朴素的Java能力,解决最真实的业务痛点。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进