简介:一套基于Java的疫苗接种与儿童体检系统设计实现完整项目文档,面向具备Java基础的后端工程师、医疗信息化开发者及智慧健康研究人员,用于解决传统手工记录效率低、信息易错漏等公共健康管理痛点。文档从项目背景、目标与意义入手,深入分析数据采集规范、安全隐私、高并发、多源数据整合等挑战,并给出基于Spring Boot+Vue前后端分离架构的完整设计方案。内容覆盖用户权限、疫苗接种、儿童体检、电子健康档案、数据分析可视化、日志审计等核心模块,配有数据库表结构设计与前后端代码示例,可直接迁移到公共卫生、基层医疗等场景。资源仅含一份docx文档,压缩包约79KB,结构清晰便于查阅;已有51人学习使用,适合毕设参考或项目落地前的技术预研。 上个月帮一个做社区卫生信息系统外包的朋友梳理需求,他提到一个挺典型的场景:接种门诊的医生还在用纸质台账登记儿童接种记录,儿保科那边体检数据也是拿 Excel 记的,两套数据互不相通。到月底统计接种率的时候,只能让人工翻本子。这个项目就是围绕“疫苗接种和儿童体检”这两条主线做的一套 Java 管理系统,包含完整程序、MySQL 数据库和 Swing 图形界面,代码全部可运行。如果你正在做数据库课程设计、Java 课程设计,或者刚入行想了解一个完整的“界面 + 业务 + 数据库”项目怎么分层,这篇很值得跟着走一遍。
我先把话放前面:这套系统的定位是“卫生服务中心内部管理工具”,不是什么互联网医疗平台。所以设计上完全抛弃了复杂的分布式架构和前后端分离,核心诉求只有一个——让医生打开电脑就能快速完成建档、登记、查询、统计四件事。下面从业务边界开始,逐步拆解数据模型、核心代码和 GUI 设计。
1. 业务定位:先搞清楚这是一个管理系统,不是预约平台
动手写任何代码之前,第一件事是圈定系统边界。很多人做这类项目一上来就想做在线预约、疫苗库存自动订货、跨机构数据共享,结果做到一半发现根本收不了尾。
这个系统最终确定的用户角色只有三类:接种医生、儿保医生、系统管理员。对应的核心功能我列一下:
- 儿童档案管理:新建档案、修改信息、注销迁出,支持按姓名、儿童编号、社区模糊查询。
- 接种登记:选择儿童和疫苗,记录接种日期、疫苗批号、接种部位、接种医生、不良反应,并自动扣减库存。
- 接种计划提醒:根据儿童出生日期计算当前月龄,对照国家免疫规划疫苗程序,自动列出“该打还没打”的疫苗。
- 体检登记:记录身高、体重、头围,系统自动计算 BMI,并结合同月龄参考标准给出“发育正常 / 偏瘦 / 超重 / 生长迟缓”的初步评估。
- 统计查询:按社区统计建档儿童数、按疫苗统计接种人次数、筛选近一周应种未种儿童。
没有做预约、没有做移动端、没有做跨机构数据交换,这些功能在一个基层门诊的真实工作流里属于低频需求,硬塞进来只会拖慢核心流程。做一个被医生真正使用的系统,比做一个“看起来很全”的演示项目更有价值。
这个边界也直接影响了下文所有的表结构设计。比如“接种计划提醒”不依赖复杂的规则引擎,而是把国家免疫规划程序直接转成一张配置表,用月龄做匹配。简单,但够用,这就是管理类系统的设计哲学。
2. 技术选型:Swing + MySQL 的选择逻辑与搭建要点
技术选型这块我直接说结论:JDK 8 + MySQL 5.7/8.0 + Java Swing + JDBC,IDE 用 IDEA,数据库可视化工具随意。这套组合放在今天看确实“复古”,但在这个项目场景下非常合理。
为什么不选 Spring Boot + Vue?原因很实际。第一,系统部署在卫生服务中心的局域网单机上,峰值并发不超过十几个人,Spring Boot 那套东西在这里产生不了任何性能优势,反而把部署复杂度拉高了几个量级。第二,Swing 做桌面程序,天然自带窗口、表格、表单组件,对一个“打开即用”的内部工具来说,交互效率比网页端高。第三,也是很多课程设计最关心的,Swing 界面在答辩时可以直接运行演示,评审老师一眼就能看到效果。
环境搭建有几个细节容易被新手卡住,我单独说一下。
MySQL 驱动版本决定了两处写法。如果你用 mysql-connector-java 5.x,驱动类是com.mysql.jdbc.Driver,URL 不需要带时区参数。如果用 8.x,驱动类必须改成com.mysql.cj.jdbc.Driver,URL 里要加上serverTimezone=Asia/Shanghai,否则会报时区错误。
JDK 版本建议用 8,Swing 在 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" + "?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; 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 List<Baby> search(String keyword) { String sql = "SELECT * FROM baby_info WHERE status=1 " + "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()) { List<Baby> 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 status=1 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 List<VaccineInfo> getDueVaccines(int babyId, Date birthday) { int ageMonth = DateUtil.getAgeMonth(birthday, new Date()); List<VaccineInfo> all = vaccineDao.findEnabled(); List<VaccineInfo> 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 "暂无标准数据"; List<String> 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()方法里更新表格数据。
SwingWorker<List<StatItem>, Void> worker = new SwingWorker<>() { @Override protected List<StatItem> 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 没指定characterEncoding=utf8。修复办法就是 DBUtil 里 URL 追加参数,同时建库语句明确指定 utf8mb4。需要提醒的是,MySQL 8.0 默认字符集是 utf8mb4,但 5.7 之前许多环境默认是 latin1,这就导致不少项目复制到别人电脑上就乱码了。
这四个坑有一个共通的排查思路:不要凭感觉猜,而是分层定位。先确认数据在数据库里的真实状态,再看代码传入的值,最后检查连接参数。把这三层查一遍,90% 的问题都能找到根源。
回到整个项目,你会发现它的价值不在于用了多牛的技术,而在于把一条真实的业务流程完整落了地——从儿童建档,到接种计划提醒,再到体检评估,数据在六张表之间流转,界面把每一步操作都变成了鼠标点击和键盘录入。如果你要复现或者改造这个项目,建议按“数据库 → 数据访问层 → 业务逻辑 → GUI”的顺序走,先把一条主流程跑通,再扩展其他功能。最后再分享一个个人小技巧:Swing 项目的开发期一定要开启JFrame的setDefaultCloseOperation(EXIT_ON_CLOSE),调试时会省掉很多杀进程的时间。
本文还有配套的精品资源,点击获取