Java Swing+MySQL实战:儿童疫苗接种与体检管理系统全解析
发布时间:2026/9/21 1:36:58来源:尧图网络
简介一套基于Java的疫苗接种与儿童体检系统设计实现完整项目文档面向具备Java基础的后端工程师、医疗信息化开发者及智慧健康研究人员用于解决传统手工记录效率低、信息易错漏等公共健康管理痛点。文档从项目背景、目标与意义入手深入分析数据采集规范、安全隐私、高并发、多源数据整合等挑战并给出基于Spring BootVue前后端分离架构的完整设计方案。内容覆盖用户权限、疫苗接种、儿童体检、电子健康档案、数据分析可视化、日志审计等核心模块配有数据库表结构设计与前后端代码示例可直接迁移到公共卫生、基层医疗等场景。资源仅含一份docx文档压缩包约79KB结构清晰便于查阅已有51人学习使用适合毕设参考或项目落地前的技术预研。 上个月帮一个做社区卫生信息系统外包的朋友梳理需求他提到一个挺典型的场景接种门诊的医生还在用纸质台账登记儿童接种记录儿保科那边体检数据也是拿 Excel 记的两套数据互不相通。到月底统计接种率的时候只能让人工翻本子。这个项目就是围绕“疫苗接种和儿童体检”这两条主线做的一套 Java 管理系统包含完整程序、MySQL 数据库和 Swing 图形界面代码全部可运行。如果你正在做数据库课程设计、Java 课程设计或者刚入行想了解一个完整的“界面 业务 数据库”项目怎么分层这篇很值得跟着走一遍。我先把话放前面这套系统的定位是“卫生服务中心内部管理工具”不是什么互联网医疗平台。所以设计上完全抛弃了复杂的分布式架构和前后端分离核心诉求只有一个——让医生打开电脑就能快速完成建档、登记、查询、统计四件事。下面从业务边界开始逐步拆解数据模型、核心代码和 GUI 设计。1. 业务定位先搞清楚这是一个管理系统不是预约平台动手写任何代码之前第一件事是圈定系统边界。很多人做这类项目一上来就想做在线预约、疫苗库存自动订货、跨机构数据共享结果做到一半发现根本收不了尾。这个系统最终确定的用户角色只有三类接种医生、儿保医生、系统管理员。对应的核心功能我列一下儿童档案管理新建档案、修改信息、注销迁出支持按姓名、儿童编号、社区模糊查询。接种登记选择儿童和疫苗记录接种日期、疫苗批号、接种部位、接种医生、不良反应并自动扣减库存。接种计划提醒根据儿童出生日期计算当前月龄对照国家免疫规划疫苗程序自动列出“该打还没打”的疫苗。体检登记记录身高、体重、头围系统自动计算 BMI并结合同月龄参考标准给出“发育正常 / 偏瘦 / 超重 / 生长迟缓”的初步评估。统计查询按社区统计建档儿童数、按疫苗统计接种人次数、筛选近一周应种未种儿童。没有做预约、没有做移动端、没有做跨机构数据交换这些功能在一个基层门诊的真实工作流里属于低频需求硬塞进来只会拖慢核心流程。做一个被医生真正使用的系统比做一个“看起来很全”的演示项目更有价值。这个边界也直接影响了下文所有的表结构设计。比如“接种计划提醒”不依赖复杂的规则引擎而是把国家免疫规划程序直接转成一张配置表用月龄做匹配。简单但够用这就是管理类系统的设计哲学。2. 技术选型Swing MySQL 的选择逻辑与搭建要点技术选型这块我直接说结论JDK 8 MySQL 5.7/8.0 Java Swing JDBCIDE 用 IDEA数据库可视化工具随意。这套组合放在今天看确实“复古”但在这个项目场景下非常合理。为什么不选 Spring Boot Vue原因很实际。第一系统部署在卫生服务中心的局域网单机上峰值并发不超过十几个人Spring Boot 那套东西在这里产生不了任何性能优势反而把部署复杂度拉高了几个量级。第二Swing 做桌面程序天然自带窗口、表格、表单组件对一个“打开即用”的内部工具来说交互效率比网页端高。第三也是很多课程设计最关心的Swing 界面在答辩时可以直接运行演示评审老师一眼就能看到效果。环境搭建有几个细节容易被新手卡住我单独说一下。MySQL 驱动版本决定了两处写法。如果你用 mysql-connector-java 5.x驱动类是com.mysql.jdbc.DriverURL 不需要带时区参数。如果用 8.x驱动类必须改成com.mysql.cj.jdbc.DriverURL 里要加上serverTimezoneAsia/Shanghai否则会报时区错误。JDK 版本建议用 8Swing 在 9 之后的模块化体系里需要额外加模块参数没必要给自己找麻烦。IDEA 里创建项目时选 Empty Project然后手动添加 MySQL 驱动 jar 到 Libraries 里就行。开发工具IDEA 或 Eclipse 均可IDEA 的 GUI Designer 对 Swing 有可视化拖拽支持但实际开发我建议纯手写布局代码更好控制。数据库客户端Navicat 或 DataGrip 都行用来执行建表脚本和看数据。JDK 版本8 最佳兼容性最好Swing 组件表现最稳定。这套技术栈还有一个隐性的好处就是它逼着你把 JDBC、事务、Swing 事件监听这些 Java 基础真正吃透。面试时被问到“数据库连接怎么管理”“事务边界怎么控制”你能拿这个项目里的真实代码来讲比背八股文有说服力得多。3. 数据库设计六张表如何把“建档-接种-体检-评估”串成闭环数据库是整个系统的地基表结构设计的好坏直接决定业务代码的复杂度。我最终的库名是vaccine_system字符集统一用 utf8mb4避免中文乱码。全套共六张表儿童档案表、疫苗信息表、接种记录表、体检记录表、用户表、生长发育标准参考表。先看儿童档案表这是全系统的主表。儿童编号我用baby_code做唯一键格式是“年份四位流水号”比如 20240001。这里有个小设计虽然id是自增主键但业务上医生习惯用编号来检索所以保留业务唯一键逻辑删除用status字段控制而不是物理删除记录。CREATE TABLE baby_info ( id INT PRIMARY KEY AUTO_INCREMENT, baby_code VARCHAR(20) NOT NULL UNIQUE, baby_name VARCHAR(30) NOT NULL, gender TINYINT NOT NULL COMMENT 0-男 1-女, birthday DATE NOT NULL, guardian_name VARCHAR(30), guardian_phone VARCHAR(11), address VARCHAR(100), community VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 1 COMMENT 1-正常 0-注销 );疫苗信息表是整个“计划提醒”功能的数据基础。关键字段是dose_seq剂次、plan_month建议接种月龄、interval_months与上一剂最小间隔月数。国家免疫规划疫苗程序很规律比如乙肝疫苗三针分别在出生时、1 月龄、6 月龄转成数据就是三条记录。CREATE TABLE vaccine_info ( id INT PRIMARY KEY AUTO_INCREMENT, vaccine_name VARCHAR(50) NOT NULL, dose_seq INT NOT NULL COMMENT 第几剂, plan_month INT NOT NULL COMMENT 建议接种月龄, interval_months INT DEFAULT 0 COMMENT 与上一剂最小间隔月数, stock INT DEFAULT 0, alert_stock INT DEFAULT 10, manufacturer VARCHAR(50), UNIQUE KEY uk_vaccine_dose (vaccine_name, dose_seq) );接种记录表通过baby_id、vaccine_id关联上面两张表。这里有一个容易忽略的设计细节next_plan_date字段它是“下次应种日期”的冗余存储。虽然可以通过月龄计算出来但把它存下来查询“本周应种儿童”时只需要一条简单的WHERE next_plan_date BETWEEN ? AND ?避免了大量实时计算。这是一个典型的“空间换时间”设计。reaction字段用来记录异常反应真实业务里这是必须追踪的。CREATE TABLE vaccination_record ( id INT PRIMARY KEY AUTO_INCREMENT, baby_id INT NOT NULL, vaccine_id INT NOT NULL, dose_seq INT NOT NULL, vaccinate_time DATE NOT NULL, batch_no VARCHAR(30), injection_site VARCHAR(20), doctor VARCHAR(20), reaction VARCHAR(100), next_plan_date DATE, KEY idx_baby (baby_id), CONSTRAINT fk_record_baby FOREIGN KEY (baby_id) REFERENCES baby_info(id), CONSTRAINT fk_record_vaccine FOREIGN KEY (vaccine_id) REFERENCES vaccine_info(id) );体检记录表单独存放不跟接种记录混在一起原因是两条业务线的数据特征完全不同接种是“按计划、多次、短时间”体检是“按周期、评估指标、连续观察”。把两张表分开将来做生长发育趋势曲线时可以直接按exam_time排序拉数据。CREATE TABLE exam_record ( id INT PRIMARY KEY AUTO_INCREMENT, baby_id INT NOT NULL, exam_time DATE NOT NULL, height DECIMAL(5,1) NOT NULL, weight DECIMAL(5,1) NOT NULL, head_circumference DECIMAL(5,1), bmi DECIMAL(5,2), doctor VARCHAR(20), conclusion VARCHAR(200), KEY idx_exam_baby (baby_id), CONSTRAINT fk_exam_baby FOREIGN KEY (baby_id) REFERENCES baby_info(id) );最后是生长发育标准参考表growth_std我按“月龄性别”维度存了身高体重上下限。医学上不同年龄段的参考值不同把标准数值数据化放进数据库而不是写死在代码里有一个好处后续如果卫健委发布了新标准只要 UPDATE 这张表的数据即可不需要重新发布程序。CREATE TABLE growth_std ( id INT PRIMARY KEY AUTO_INCREMENT, age_month INT NOT NULL, gender TINYINT NOT NULL, height_min DECIMAL(5,1), height_max DECIMAL(5,1), weight_min DECIMAL(5,1), weight_max DECIMAL(5,1) );这张六表模型的逻辑闭环是这样的儿童建档后接种登记产生接种记录并推进计划体检登记产生体检记录并通过标准表自动评估。用户表主要控制登录权限这里不再展开。4. 数据访问层JDBC连接管理、防注入与统计查询的写法数据访问层我封装了一个DBUtil类负责连接管理所有 DAO 类都用同一套资源释放模板。很多初学项目只写个getConnection()连接用完不关跑一上午就报Too many connections所以在封装时就要把资源关闭的逻辑固化下来。public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/vaccine_system ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(ResultSet rs, Statement stmt, Connection conn) { if (rs ! null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (stmt ! null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }这里必须强调一个防注入的原则所有用户输入的条件一律用PreparedStatement的占位符?传参绝对不允许拼接 SQL 字符串。之前见过一个课程设计代码里写SELECT * FROM baby_info WHERE baby_name LIKE % keyword %这等于把整个库暴露在 SQL 注入风险之下。用占位符的写法是public ListBaby search(String keyword) { String sql SELECT * FROM baby_info WHERE status1 AND (baby_name LIKE ? OR baby_code LIKE ? OR community LIKE ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { String like % keyword %; ps.setString(1, like); ps.setString(2, like); ps.setString(3, like); try (ResultSet rs ps.executeQuery()) { ListBaby list new ArrayList(); while (rs.next()) { list.add(mapRow(rs)); } return list; } } catch (SQLException e) { e.printStackTrace(); return Collections.emptyList(); } }注意我用了 try-with-resources 语法Connection、PreparedStatement、ResultSet都实现了AutoCloseable作用域结束自动关闭代码简洁也不容易漏。统计功能有几个实用 SQL。按社区统计建档儿童数SELECT community, COUNT(*) AS total, SUM(CASE WHEN status1 THEN 1 ELSE 0 END) AS valid_count FROM baby_info GROUP BY community;查询近 7 天应种未种的儿童这条 SQL 需要联合三张表。核心思路是先查出“今天到未来 7 天之间应该接种某种疫苗某剂次”的儿童再排除那些在对应疫苗和剂次上已经有接种记录的。SELECT b.baby_name, b.baby_code, b.birthday, v.vaccine_name, v.dose_seq, DATE_ADD(DATE_ADD(b.birthday, INTERVAL v.plan_month MONTH), INTERVAL 7 DAY) AS due_date FROM baby_info b JOIN vaccine_info v LEFT JOIN vaccination_record r ON r.baby_id b.id AND r.vaccine_id v.id AND r.dose_seq v.dose_seq WHERE b.status 1 AND r.id IS NULL AND DATE_ADD(b.birthday, INTERVAL v.plan_month MONTH) BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 7 DAY);这种 SQL 的写法在真实报表里很常见值得好好理解一下LEFT JOIN加右表IS NULL判断是标准的“找没干过某事的人”套路。5. 三个核心算法月龄、接种计划提醒、生长发育评估这个系统真正有含金量的业务代码集中在三个算法上逐个拆开讲。第一个是月龄计算。这个看着简单实际极易出错。如果直接拿两个日期差除以 30 天来算月龄结果是不准确的——2 月只有 28 天除以 30 会让 2 月出生的孩子月龄被高估。正确的算法是用 Calendar 的年、月差再做日期边界修正public static int getAgeMonth(Date birthday, Date now) { Calendar c1 Calendar.getInstance(); c1.setTime(birthday); Calendar c2 Calendar.getInstance(); c2.setTime(now); int months (c2.get(Calendar.YEAR) - c1.get(Calendar.YEAR)) * 12 (c2.get(Calendar.MONTH) - c1.get(Calendar.MONTH)); if (c2.get(Calendar.DAY_OF_MONTH) c1.get(Calendar.DAY_OF_MONTH)) { months--; } return months; }边界情况是出生日是 3 月 31 日今天是 5 月 30 日按上面的算法月份差是 2但因为当前日 30 出生日 31所以减 1结果是 1 个月零 29 天系统只返回整月数 1。不能返回 2否则会让孩子提前一个月接种这是医疗场景绝对不能犯的错误。第二个是接种计划提醒。这个算法的核心是“遍历疫苗配置表判断当前该打哪一剂”。判断逻辑分三种情况第一剂没打过直接进入待种列表打过某剂但下一剂的时间间隔没到跳过已打完所有剂次标记为全程完成。public ListVaccineInfo getDueVaccines(int babyId, Date birthday) { int ageMonth DateUtil.getAgeMonth(birthday, new Date()); ListVaccineInfo all vaccineDao.findEnabled(); ListVaccineInfo dueList new ArrayList(); for (VaccineInfo v : all) { if (v.getPlanMonth() ageMonth) continue; VaccinationRecord last recordDao.findLastByBabyAndVaccine(babyId, v.getId()); if (last null v.getDoseSeq() 1) { dueList.add(v); } else if (last ! null v.getDoseSeq() last.getDoseSeq() 1 isIntervalOk(last.getVaccinateTime(), v.getIntervalMonths())) { dueList.add(v); } } return dueList; }间隔判断的方法isIntervalOk用 Calendar 加月份后与当前日期比较与月龄计算是同一个思路。这里有一个业务规则需要说明plan_month是建议月龄interval_months是最小间隔月数两个条件要同时满足才算“到期”。比如百白破第 2 剂建议 3 月龄且与第 1 剂最少间隔 1 个月——如果孩子 2.5 月龄就打了第一剂即使现在已经满了 2 月龄第二剂也不能算到期。第三个是生长发育评估。BMI 的计算很简单但要注意单位换算身高单位是厘米体重单位是公斤计算 BMI 时身高必须转成米再平方。public String evaluate(int ageMonth, int gender, double height, double weight) { double heightM height / 100.0; double bmi weight / (heightM * heightM); bmi Math.round(bmi * 100) / 100.0; GrowthStd std stdDao.findByAgeAndGender(ageMonth, gender); if (std null) return 暂无标准数据; ListString problems new ArrayList(); if (weight std.getWeightMin()) problems.add(低体重); if (weight std.getWeightMax()) problems.add(超重); if (height std.getHeightMin()) problems.add(生长迟缓); if (bmi 24) problems.add(肥胖); return problems.isEmpty() ? 发育正常 : String.join(、, problems); }这里有个识别重点评估不是简单地把体重身高跟标准区间比对而是结合 BMI 来判断肥胖避免“单纯体重超标但身高也高”的孩子被误判。标准表的索引字段是(age_month, gender)联合唯一所以查询直接返回一条记录。要留意的是评估建议只是辅助参考不能作为临床诊断界面里要显示这句话这是医疗系统该有的严谨。这三个算法是整个系统的灵魂也是回答“为什么这个系统有存在价值”的关键医生手动算月龄可能出错但对电脑来说这是一次确定的计算不会因为下班疲惫而算错。6. 界面怎么设计才能让医生顺畅操作主窗体布局与事件联动GUI 设计的原则不是好看而是减少误操作、提高录入速度。这套系统的界面我用的是 Swing 原生组件没有引入任何第三方美化库结构上分三层。第一层是登录窗体。用户名、密码两个输入框加一个登录按钮回车键绑定登录事件。密码用 MD5 加盐存储登录时把输入密码做同样的哈希再比对。这块不多讲属于标准写法。第二层是主窗体用的是经典的“左侧导航 右侧内容卡片”布局。左侧是个 JTree 导航树右侧是个 CardLayout 容器切换节点时通过 CardLayout 的show()方法切换到对应的面板。这样的好处是主窗体只创建一次切换时不会重新加载操作响应快。tree.addTreeSelectionListener(e - { DefaultMutableTreeNode node (DefaultMutableTreeNode) tree.getLastSelectedPathComponent(); if (node null || node.isRoot()) return; String key (String) node.getUserObject(); CardLayout cl (CardLayout) mainPanel.getLayout(); cl.show(mainPanel, key); });要注意的是 JTree 节点不能直接用字符串当getUserObject因为显示名和面板 key 未必一致。我建议建一个双层结构显示名用中文key 用英文。比如“儿童档案登记”显示在树上key 是babyList这样后续加权限控制时也方便。第三层是各业务面板。以儿童档案列表为例布局是“顶部工具栏 中部表格 底部搜索分页”。工具栏放“新增、修改、注销、刷新”四个按钮表格用 JTable 绑 DefaultTableModel搜索框放在底部输入关键字回车触发查询。这个布局模式在三个业务面板里复用视觉统一学习成本低。DefaultTableModel model (DefaultTableModel) table.getModel(); model.setRowCount(0); for (Baby b : list) { model.addRow(new Object[]{ b.getId(), b.getBabyCode(), b.getBabyName(), b.getGender() 0 ? 男 : 女, DateUtil.format(b.getBirthday()), b.getGuardianName(), b.getGuardianPhone(), b.getCommunity() }); }这里有一个新手最常见的坑DefaultTableModel的表头设置必须在构造时传入或者在首次setRowCount(0)前设置好。否则先清空再 setValueAt 会报数组越界。我习惯把表头数组定义成常量模型创建和重置都用同一个数组。表单面板我用的是 JDialog 弹出窗口GridBagLayout两列布局标签一列、输入控件一列。必填项比如儿童姓名、出生日期标签前加红色*前缀。保存时先做非空校验不通过就在对应输入框获得焦点而不是弹一堆 MessageDialog。一个细节值得展开接种登记面板里疫苗下拉框的选择事件。选中疫苗后系统会自动带出该疫苗的默认接种部位、建议剂次同时在界面右侧实时显示“该儿童剩余库存 xx 支”。这个联动是靠ItemListener实现的每切换一个疫苗就重新查一次vaccine_info表。cmbVaccine.addItemListener(e - { if (e.getStateChange() ItemEvent.SELECTED) { VaccineInfo v (VaccineInfo) cmbVaccine.getSelectedItem(); lblStock.setText(当前库存 v.getStock() 支); if (v.getStock() v.getAlertStock()) { lblStock.setForeground(Color.RED); } } });界面设计最后再提一个容易被忽略的地方表格列表双击行打开详情。我把鼠标监听加在表格上双击后在右侧面板直接展示该儿童的全部接种历史和体检历史这个功能在给医生核对既往史时非常高频。布局上我用的是一个垂直 BoxLayout上面是基本信息展示区下面是两个 JTabbedPane 页签分别放接种历史和体检历史。7. 真实排错记录事务、线程、日期、编码四个深坑这个项目在测试和联调阶段踩了四个很有代表性的坑我按排查链路完整记录下来。如果你做的是类似的管理系统下面这些问题大概率也会遇到。第一个坑是事务忘了提交导致库存扣减对不上。现象描述接种登记成功后列表里能看到该疫苗库存减少了但数据库里stock字段根本没变。排查过程先怀疑 SQL 的UPDATE条件写错打印 SQL 后单独执行也能生效后来在代码里加日志发现conn.commit()没写连接一直处于自动提交关闭状态事务回滚时数据就没了。这个坑的修复其实很简单但也暴露出一个习惯问题涉及多张表更新的操作必须显式地写事务边界。Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); // 1. 插入接种记录 // 2. UPDATE vaccine_info SET stock stock - 1 WHERE id ? AND stock 0 // 如果 update 影响行数为 0说明库存不足抛出异常 conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); }注意UPDATE ... WHERE stock 0这个条件它让“扣减库存”这个操作变成了原子操作。就算两个窗口同时在接种也不会出现库存扣成负数的情况。第二个坑是 Swing 的 EDT 线程问题。现象是点击“刷新统计”按钮后界面整个卡死转圈圈要等十几秒才恢复。排查发现是查询逻辑直接写在事件监听器里数据量大时阻塞了事件分发线程。修复方案是引入 SwingWorker 做异步查询查询完成后在done()方法里更新表格数据。SwingWorkerListStatItem, Void worker new SwingWorker() { Override protected ListStatItem doInBackground() { return statDao.queryCommunityStats(); } Override protected void done() { try { fillTable(get()); } catch (Exception e) { JOptionPane.showMessageDialog(mainFrame, 统计查询失败); } } }; worker.execute();这个坑的启示是哪怕是一个单机桌面程序也必须有异步意识。所有超过几百毫秒的数据库操作都不应该直接跑在 EDT 上。第三个坑是日期类型的处理。java.util.Date 和 java.sql.Date 的互相转换很容易踩。PreparedStatement 的setDate()方法接收的是java.sql.Date但业务层常用的new Date()是java.util.Date直接传进去编译不报错运行时数据却变成1970-01-01这种诡异值。处理方式是用一个 DateUtil 工具类统一转换和格式化public static java.sql.Date toSqlDate(java.util.Date d) { return d null ? null : new java.sql.Date(d.getTime()); }第四个坑是中文乱码。表现是界面上显示正常但通过命令行查询数据库看到的是??。原因分析建库用了默认字符集而 JDBC 连接 URL 没指定characterEncodingutf8。修复办法就是 DBUtil 里 URL 追加参数同时建库语句明确指定 utf8mb4。需要提醒的是MySQL 8.0 默认字符集是 utf8mb4但 5.7 之前许多环境默认是 latin1这就导致不少项目复制到别人电脑上就乱码了。这四个坑有一个共通的排查思路不要凭感觉猜而是分层定位。先确认数据在数据库里的真实状态再看代码传入的值最后检查连接参数。把这三层查一遍90% 的问题都能找到根源。回到整个项目你会发现它的价值不在于用了多牛的技术而在于把一条真实的业务流程完整落了地——从儿童建档到接种计划提醒再到体检评估数据在六张表之间流转界面把每一步操作都变成了鼠标点击和键盘录入。如果你要复现或者改造这个项目建议按“数据库 → 数据访问层 → 业务逻辑 → GUI”的顺序走先把一条主流程跑通再扩展其他功能。最后再分享一个个人小技巧Swing 项目的开发期一定要开启JFrame的setDefaultCloseOperation(EXIT_ON_CLOSE)调试时会省掉很多杀进程的时间。本文还有配套的精品资源点击获取
网站建设高端定制企业官网