ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java Swing与MySQL实现学生健康档案管理系统:从数据模型到JDBC持久层

Java Swing与MySQL实现学生健康档案管理系统:从数据模型到JDBC持久层 简介这是一份基于MySQL与Java Swing开发的学生健康档案管理系统主要面向计算机相关专业的在校学生与初入开发者适合作为毕业设计、课程设计或项目立项演示。系统围绕学生信息、体检信息、病历信息进行集中管理覆盖登录注册、添加修改、多选项高级查询、多选删除、批量导入导出Excel以及动态折线图可视化等完整功能其中体检数据支持学号、姓名、性别、系别、年龄、身高、体重、胸围、日期等字段病历数据支持诊断、日期等核心信息能够帮助用户高效完成健康档案的归档与检索替代传统纸质记录方式。资源压缩包共包含98个文件主要有30个Java源码、38个编译class文件、XML界面布局、Properties配置、SQL建库脚本、XLS样例数据以及PNG/JPG界面截图包大小仅1.27MB便于快速部署与剖析。据悉已有76人学习下载。整个项目经功能验证通过附有数据库表设计和E-R图代码结构清晰既可用于答辩展示也适合二次开发是学习Java与MySQL整合开发的实用参考。1. 学生健康档案管理系统到底在做什么别把它当 CRUD 练手项目每逢开学季校医室和班主任最头疼的不是学生生病而是那堆纸质体检表和疫苗接种记录。基于 MySQL Java swing 实现的学生健康档案管理系统HealthArchives说白了就是把学生的身高体重、视力、既往病史、过敏药物、疫苗接种记录这些零散数据从 Excel 和纸质表格里搬进 MySQL再用 Java swing 做一个桌面端操作界面让校医能录、能查、能改、能统计。这类系统在高校后勤、中学卫生室、甚至企业员工体检场景里都有实际需求找外包做一套至少五位数报价自己用 Java 课设的精力搭一套完全可行。“这不就是个 CRUD 吗”是大多数人第一反应但真做起来你会发现健康档案比普通业务系统多三个硬门槛数据结构随年龄段变化小学生和大学的身高体重评价标准不一样、隐私字段多既往病史、过敏史属于敏感信息、Excel 导入导出的频率远高于普通管理系统。适合的人群也明确有 Java SE 基础、想练 JDBC 和 Swing 整合的在校学生或者卫生室想低成本信息化但预算有限的一线老师。下面从数据模型到界面到持久层一条线拆完。2. 建表之前先想清楚健康档案的数据模型MySQL 表结构设计是这套系统的地基2.1 健康档案的核心实体关系和字段取舍学生健康档案最少要拆三张主表和两张关联表student存基本信息health_record存每次体检的度量数据medical_history存既往病史和过敏史。为什么不能把全部字段塞进一张大表因为健康数据是一对多的一个学生从大一入学到大四毕业至少有四到五次体检记录如果每次体检都复制一份姓名、学号、学院信息表会越来越膨胀改个联系方式要 UPDATE 几十行。我一般会这样设计字段以下建表 SQL 是整套系统的地基建议直接复制后根据本校字段微调-- 学生基础信息表 CREATE TABLE student ( student_id VARCHAR(20) PRIMARY KEY COMMENT 学号如 2025010101, name VARCHAR(50) NOT NULL COMMENT 姓名, gender CHAR(1) NOT NULL COMMENT 性别 M/F, birth_date DATE NOT NULL COMMENT 出生日期, college VARCHAR(100) NOT NULL COMMENT 学院, major VARCHAR(100) NOT NULL COMMENT 专业, class_name VARCHAR(50) NOT NULL COMMENT 班级, phone VARCHAR(15) DEFAULT NULL COMMENT 联系电话, emergency_name VARCHAR(50) DEFAULT NULL COMMENT 紧急联系人, emergency_phone VARCHAR(15) DEFAULT NULL COMMENT 紧急联系人电话, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_college (college), KEY idx_class (class_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci; -- 体检记录表 CREATE TABLE health_record ( record_id INT AUTO_INCREMENT PRIMARY KEY, student_id VARCHAR(20) NOT NULL, check_date DATE NOT NULL COMMENT 体检日期, height_cm DECIMAL(5,1) NOT NULL COMMENT 身高 cm, weight_kg DECIMAL(5,1) NOT NULL COMMENT 体重 kg, bmi DECIMAL(4,1) NOT NULL COMMENT BMI 值程序计算后存入, vision_left DECIMAL(4,1) DEFAULT NULL COMMENT 左眼视力, vision_right DECIMAL(4,1) DEFAULT NULL COMMENT 右眼视力, blood_pressure VARCHAR(10) DEFAULT NULL COMMENT 血压如 120/80, heart_rate INT DEFAULT NULL COMMENT 静息心率, lung_capacity INT DEFAULT NULL COMMENT 肺活量 ml, remark VARCHAR(500) DEFAULT NULL COMMENT 校医备注, created_by VARCHAR(50) DEFAULT NULL COMMENT 录入人, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_student (student_id), KEY idx_check_date (check_date), CONSTRAINT fk_hr_student FOREIGN KEY (student_id) REFERENCES student(student_id) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci; -- 既往病史/过敏史表 CREATE TABLE medical_history ( history_id INT AUTO_INCREMENT PRIMARY KEY, student_id VARCHAR(20) NOT NULL, history_type ENUM(disease,allergy,surgery,vaccination) NOT NULL, item_name VARCHAR(100) NOT NULL COMMENT 疾病名/过敏原/疫苗名称, diagnosed_date DATE DEFAULT NULL COMMENT 确诊或接种时间, status VARCHAR(20) DEFAULT active COMMENT active/recovered, note VARCHAR(255) DEFAULT NULL, KEY idx_student (student_id), CONSTRAINT fk_mh_student FOREIGN KEY (student_id) REFERENCES student(student_id) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;这段 SQL 里的逻辑说明student_id不用 INT 自增而是 VARCHAR因为学号本身就是天然的业务主键而且大学学号长度固定、带年份和学院编码用自增 ID 反而要多做一次映射health_record里单独存bmi而不是每次查询时算是因为 BMI 的截断规则保留一位小数还是四舍五入一旦变化历史数据还能保持当时录入的口径外键用ON DELETE CASCADE学生退学删除档案时体检记录同步清掉避免出现孤儿数据。参数说明DECIMAL(5,1)表示最大 999.9身高体重完全够用ENUM(disease,allergy,surgery,vaccination)约束了历史记录的类型避免业务上出现“鼻炎”被录成“手术”这种脏数据。字符集统一utf8mb4而不是utf8因为utf8在 MySQL 里最多存 3 字节遇到少数生僻字直接报错或问号。排序规则用_unicode_ci是让中文按拼音排序班级名单列表输出时一致。2.2 为什么字段默认值和约束比想象中重要健康档案系统最容易翻车的地方不是主键而是默认值和约束。举个例子血压字段设计成VARCHAR(10)而不是 INT因为血压记录经常是 “120/80” 这种带斜杠的字符串拆成收缩压和舒张压两个 INT 字段当然更规范但校医录入时没有精力分两个框填一个文本框输入得越快越好VARCHAR 能直接存原始格式。这时候如果数据库里不给DEFAULT NULL程序里插入时漏掉血压就会直接抛 SQLException。还有录入人字段created_by桌面应用里是谁登录操作的就写谁这听起来无所谓但到了期末要核查“某条异常记录是谁录的”时没有这个字段就得翻日志。建议所有业务表都加create_time和update_timeMySQL 8.0 的DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP可以在行级别自动维护这两个时间省掉 Java 端的new Date()手动拼接。我在第一次给学生做这个系统时吃过一次字符集的亏Windows 上 MySQL 5.7 建库时默认latin1Java 端用 UTF-8 插入“王明皓”的“皓”字直接变问号。后来统一在建库语句里显式指定DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci才彻底解决。注意如果已经建了表要用ALTER TABLE student CONVERT TO CHARACTER SET utf8mb4;转字符集否则只改表的默认字符集但存量列的字符集没变问题依旧。-- 建库时指定字符集不要依赖 MySQL 默认配置 CREATE DATABASE health_archives DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 如果表已经建好且是 latin1转换时务必一条条查出来 SELECT table_name FROM information_schema.tables WHERE table_schema health_archives AND table_collation ! utf8mb4_unicode_ci;alter table student convert to character set utf8mb4 collate utf8mb4_unicode_ci; alter table health_record convert to character set utf8mb4 collate utf8mb4_unicode_ci; alter table medical_history convert to character set utf8mb4 collate utf8mb4_unicode_ci;查出来的表逐张ALTER TABLE ... CONVERT TO一下这是转字符集的唯一靠得住的方式ALTER TABLE ... DEFAULT CHARACTER SET只影响新加列存量列的存储编码还是老的。这属于典型的“改了配置但没生效”的坑后面排查章节还会提到 MySQL 服务端character_set_server参数。2.3 敏感字段的存储策略先把加密后路想好健康档案里的既往病史、过敏史属于个人敏感信息。课设项目往往不加密但放到真实卫生室场景就要考虑。常见的做法是过敏史和疾病名称不能明文存数据库至少做一层 AES 加密密钥放在 Java 端的配置文件中不进 MySQL。这是合规要求也是给系统留后路——你永远不知道什么时候学校要求做等级保护备案。我一般用 JDK 自带的javax.crypto.Cipher做 AES-128 加密密钥硬编码在config.properties里而不是代码中。这样如果未来要换密钥只需要改配置文件重启程序不用重新编译 jar。加密后的item_name字段需要VARBINARY(255)而不是 VARCHAR因为密文包含不可见字节VARCHAR 的隐式转换会截断或破坏数据。3. Java Swing 界面分层搭建从 JFrame 主窗口到 TableModel 的绑定思路3.1 桌面端 UI 的分层架构为什么不能用“一个类一个窗口”的方式堆Swing 项目最常见的翻车写法是一个MainFrame类里写了所有窗口、所有按钮监听、所有 JDBC 代码几百行全挤在actionPerformed里。短期看是能跑但一旦要加一个“按学院筛选”的功能你就得在这堆代码里找那个JButton的监听器在哪。更现实的是Swing 开发里界面线程和数据库操作线程是冲突的——JDBC 查询是阻塞操作如果直接在主线程里执行窗口会卡成“白框没响应”在 Windows 上系统直接提示“程序未响应”。我常用的分层思路是界面层UI 业务逻辑层Service 数据访问层DAO。Swing 的JFrame、JPanel、JTable只负责把数据展示出来点按钮之后调用 Service 层的对应方法Service 层内部做参数校验比如学号不能为空、身高不能为负数然后调 DAO 层执行 SQL。每一层的类名和职责清清楚楚// 一个典型的主窗口骨架 public class MainFrame extends JFrame { private JTabbedPane tabbedPane; private JTable studentTable; private StudentDAOService service; // 业务层 public MainFrame() { service new StudentDAOService(); initUI(); loadStudentData(); // 加载全部学生 } private void initUI() { setTitle(学生健康档案管理系统 - HealthArchives); setSize(1024, 768); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 左侧导航或者顶部 Tab 页学生信息 / 体检记录 / 病史档案 / 统计分析 tabbedPane new JTabbedPane(); tabbedPane.addTab(学生档案, createStudentPanel()); tabbedPane.addTab(体检记录, createHealthPanel()); tabbedPane.addTab(统计报表, createReportPanel()); setContentPane(tabbedPane); } }这一段代码说的是入口逻辑主窗口用JTabbedPane分四个页签比多个JFrame弹来弹去更直观也避免了dispose()没调用导致内存累积的坑。注意setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)在正式版里最好换成DO_NOTHING_ON_CLOSE在windowClosing事件里弹确认框“确认退出吗”否则用户误关窗口程序直接没了未保存的数据全丢。接着看表格数据怎么绑定。Swing 的JTable不能用ArrayList直接往里塞必须通过TableModel接口。我很少自定义AbstractTableModel再覆写getValueAt因为每次都重写太繁琐更快的路子是用DefaultTableModel配合二维数组// 查询学生列表并绑定到表格 public void loadStudentData(String keyword) { ListStudent students service.searchStudents(keyword); String[] columns {学号, 姓名, 性别, 出生日期, 学院, 专业, 班级, 联系电话}; Object[][] rows new Object[students.size()][8]; for (int i 0; i students.size(); i) { Student s students.get(i); rows[i][0] s.getStudentId(); rows[i][1] s.getName(); rows[i][2] s.getGender().equals(M) ? 男 : 女; rows[i][3] s.getBirthDate().toString(); rows[i][4] s.getCollege(); rows[i][5] s.getMajor(); rows[i][6] s.getClassName(); rows[i][7] s.getPhone(); } studentTable.setModel(new DefaultTableModel(rows, columns)); }这里的逻辑是通过 Service 层拿到ListStudent后转成 Object 二维数组用DefaultTableModel一次绑定。注意性别字段在数据库里存的是M/F界面显示要转成“男/女”这种转换放在 DAO 层返回结果时由 Java 代码做映射比在 SQL 里用CASE WHEN灵活也让 SQL 保持最简单。参数说明DefaultTableModel(rows, columns)构造里第一个参数是数据行第二个是表头。这里有个前提——每列的类型必须是 Object如果你在 SQL 里直接SELECT一个DATE类型Java 端ResultSet.getDate()拿到的是java.sql.Date放进表格显示是 “2025-03-01”没问题。但如果直接放LocalDateDefaultTableModel默认按 Object 输出 toString可能带T前缀或者年份不正常所以更稳妥的做法是统一先toString()格式化再放进数组。3.2 Swing 线程模型SwingWorker 是解决界面卡死的唯一正解前面提了 JDBC 查询阻塞会卡界面。这里单独拆一个小节因为这是 Swing 项目面试必问点、也是实战最容易踩的坑。Swing 是单线程模型所有组件刷新必须在 EDTEvent Dispatch Thread上执行。数据库查询在网络或磁盘上耗时几十毫秒到几秒如果在按钮点击事件里直接执行 JDBCEDT 被阻塞界面就变“未响应”。常规解法是SwingWorkerT, V// 异步加载体检记录避免主界面卡死 JButton btnLoad new JButton(加载体检记录); btnLoad.addActionListener(e - { btnLoad.setEnabled(false); // 防重复点击 new SwingWorkerListHealthRecord, Void() { Override protected ListHealthRecord doInBackground() throws Exception { // 在后台线程执行查询不碰任何 UI 组件 return service.queryHealthRecords(currentStudentId); } Override protected void done() { try { ListHealthRecord records get(); bindHealthTable(records); } catch (Exception ex) { JOptionPane.showMessageDialog(MainFrame.this, 查询失败请检查数据库连接, 错误, JOptionPane.ERROR_MESSAGE); } finally { btnLoad.setEnabled(true); } } }.execute(); });doInBackground里不能碰 UI 组件否则会抛InterruptedException或导致组件状态不一致done()里get()拿到返回结果后再刷新表格。参数说明SwingWorker的泛型第一个类型是后台返回结果类型第二个是中间进度类型这里用Void忽略进度。页面一次只加载 500 条记录的话用这个结构不会卡如果数据量上万建议加分页SQL 里LIMIT ? OFFSET ?页大小一般 50 条加一个“上一页 / 下一页”按钮组因为健康档案系统里校医只看当前班级的学生翻到底的场景不多不用做虚拟滚动那么复杂。3.3 界面布局用 GridBagLayout 还是手工绝对定位有些教程一句“setLayout(null)”加绝对定位快速是快速窗口一拉大按钮就挪位换一台 Windows 10 和 Windows 11 的显示缩放125% 或 150%直接错位。Swing 官方推荐的GridBagLayout是那个“学的时候想摔键盘、用的时候真香”的布局管理器但如果你不想花一下午理解gridx/gridy/weightx/insets更务实的方案是嵌套JPanel// 学生信息录入表单的布局方式 JPanel formPanel new JPanel(new GridLayout(0, 2)); // 两列多行 formPanel.add(new JLabel(学号:)); formPanel.add(new JTextField(20)); formPanel.add(new JLabel(姓名:)); formPanel.add(new JTextField(20)); // 按此规律继续加字段 JPanel buttonPanel new JPanel(new FlowLayout(FlowLayout.RIGHT)); buttonPanel.add(new JButton(保存)); buttonPanel.add(new JButton(重置)); JPanel rootPanel new JPanel(new BorderLayout()); rootPanel.add(formPanel, BorderLayout.CENTER); rootPanel.add(buttonPanel, BorderLayout.SOUTH);逻辑说明GridLayout(0, 2)意思是列数固定 2、行数自动扩展表单标签和输入框天然对齐。FlowLayout.RIGHT把按钮推到右下角符合桌面端用户预期。这块不细讲GridBagConstraints是因为 HealthArchives 这类系统的界面复杂度用嵌套布局完全够。真要做左侧导航树 右侧内容区再用JSplitPane组合JTree和CardLayout这是另一个话题了。窗口大小建议setSize(1280, 800)而不是setResizable(false)因为校医室里可能有老旧 1366x768 的显示器存在界面截断风险。内容较多的窗口用JScrollPane包起来特别是JTable不包 ScrollPane 的话表头都不显示。4. JDBC 持久层落地PreparedStatement、事务边界和连接注意点4.1 连接管理为什么不用 DriverManager 散写连接而是写一个工具类Swing 项目里常见反面教材是每个 DAO 方法里都写一遍DriverManager.getConnection(url, user, password)然后用完就扔。MySQL 连接建立平均要几十毫秒如果录入体检记录时一次保存要插入 health_record 还要 update 学生信息的更新时间两次各开一个连接整体性能直接翻倍浪费。更稳的写法是维护一个单例的DBUtil工具类方法返回同一个Connection或者至少统一管理 URL 和账号密码public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/health_archives ?useUnicodetruecharacterEncodingutf8mb4 useSSLfalse serverTimezoneAsia/Shanghai allowPublicKeyRetrievaltrue; private static final String USER root; private static final String PASSWORD your_password; private DBUtil() {} public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }这段参数值得逐项解释characterEncodingutf8mb4指定客户端用 UTF-8 发送 SQL 和接收结果少了这个参数即使数据库字符集是 utf8mb4Java 驱动的连接字符集也可能是 latin1从 Java 界面输入中文再插进库里照样乱码。useSSLfalse是本地开发时用的MySQL 8 默认开了 SSL 会导致首次连接时多一次握手慢了不说还没必要serverTimezoneAsia/Shanghai是 Java 8 驱动必须加的不写会抛The server time zone value Öйú±ê׼ʱ¼ä这种乱码时区异常allowPublicKeyRetrievaltrue是 MySQL 8 用caching_sha2_password认证时必须的否则第一次连接报Public Key Retrieval is not allowed。连接池在桌面应用里要不要引入我的建议是不要急着上。HikariCP虽然好用但桌面应用并发量小同时操作的就那么一两个校医连接复用的收益不明显反而引入 jar 依赖和配置复杂度。等系统部署后真的出现频繁连接超时再改造不迟JDBC 裸连足够支撑几千学生的档案规模。4.2 DAO 层 CRUD 的标准写法与明显误区JDBC 的Statement和PreparedStatement选择上没有悬念任何带用户输入的条件查询、插入、更新一律用PreparedStatement。原因不只是 SQL 注入更是因为PreparedStatement会预编译同样的 SQL 执行多次时性能好于Statement每次硬解析。看一个插入体检记录的例子public void insertHealthRecord(HealthRecord record) throws SQLException { String sql INSERT INTO health_record (student_id, check_date, height_cm, weight_kg, bmi, vision_left, vision_right, blood_pressure, heart_rate, lung_capacity, remark, created_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, record.getStudentId()); ps.setDate(2, java.sql.Date.valueOf(record.getCheckDate())); // LocalDate - sql.Date ps.setBigDecimal(3, record.getHeightCm()); ps.setBigDecimal(4, record.getWeightKg()); ps.setBigDecimal(5, record.getBmi()); ps.setBigDecimal(6, record.getVisionLeft()); ps.setBigDecimal(7, record.getVisionRight()); ps.setString(8, record.getBloodPressure()); ps.setInt(9, record.getHeartRate()); ps.setInt(10, record.getLungCapacity()); ps.setString(11, record.getRemark()); ps.setString(12, record.getCreatedBy()); ps.executeUpdate(); } // try-with-resources 自动关闭 ps 和 conn }这个方法的逻辑说明try-with-resources语法保证PreparedStatement和Connection在方法结束后自动关闭比 finally 里手动close()少写三行且不会漏。注意setDate(2, java.sql.Date.valueOf(record.getCheckDate()))—— Java 8 之后大家习惯用LocalDate但 JDBC 的setDate只接受java.sql.Date所以必须搭一座桥。反过来从ResultSet读日期时也建议getDate().toLocalDate()转回来这样业务层可以全程用LocalDate避免java.sql.Date的 nullable 语义和LocalDate不一致导致 NPE。常见误区是手动拼接日期字符串VALUES ( record.getCheckDate() )这个在小规模数据上“侥幸能跑”一旦日期格式被 MySQL 的sql_mode改掉比如严格模式要求yyyy-MM-dd程序直接抛Data truncation而且这种写法也把 SQL 注入的大门开了一半。对健康档案这种含敏感字段的系统注入风险和政治无关是实打实的数据安全事件风险。另一个容易翻车的是空值处理。体检时视力没测可能是空身高体重是必填是另一回事。setBigDecimal(6, record.getVisionLeft())如果visionLeft为 null 是允许的PreparedStatement会自动把它映射成 SQL NULL。但在 Java 对象转换成 BigDecimal 时如果前端把空字符串传进来new BigDecimal()会抛NumberFormatException。所以 DAO 层之外必须有校验Service 层里判断textField.getText().trim().isEmpty()就置 null而不是硬转。这类校验写在 Service 层的好处是界面换皮肤或后续改成 RIA 客户端时校验逻辑不用改。4.3 事务边界一条体检记录不只是一个 INSERT录入一次完整体检至少涉及两个动作插入health_record一条同时可能更新student表的update_time如果字段设计成自动更新则不用管。更常见的场景是批量导入Excel 里有 200 个学生的体检数据循环插入 200 条中间有一条学号不存在外键约束直接报错。这时候如果没用事务前 150 条已经插进库了后 50 条全失败数据处于“半完成”状态校医根本不知道哪些成功哪些失败。public void batchImportHealthRecords(ListHealthRecord records) throws SQLException { String sql INSERT INTO health_record (...,...) VALUES (?,?,...); Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 try (PreparedStatement ps conn.prepareStatement(sql)) { for (HealthRecord r : records) { ps.setString(1, r.getStudentId()); // ... 各字段赋值 ps.addBatch(); } ps.executeBatch(); conn.commit(); // 全部成功才提交 } catch (SQLException e) { conn.rollback(); // 任何一条失败全部回滚 throw e; } } finally { if (conn ! null) { conn.setAutoCommit(true); // 恢复默认归还给连接池时避免污染连接状态 conn.close(); } } }逻辑说明setAutoCommit(false)之后每次executeBatch()不会立刻写入磁盘直到显式commit()。rollback()可以撤销本连接所有未提交操作。这里的默认Batch数量没有硬性限制但 200 条以下一次性没问题几千条建议每 500 条executeBatch()一次再 commit不然事务持有大量行锁可能导致 MySQL 锁等待超时Lock wait timeout exceeded。事务相关的开发习惯建议事务只写在需要原子性的地方不要在select * from查询外面包一个setAutoCommit(false)再 commit白加开销还会长期持有共享锁。健康系统里批量导入、修改学生基础信息同时更新关联的体检记录历史比如学号变更这种操作才需要事务单条选择查询不需要。另外conn.setAutoCommit(true)一定要放在 finally 里恢复。JDBCTemplate这类框架会自动管理连接状态但裸 JDBC 下如果连接池里有残留的“未提交事务”状态下一次复用就莫名其妙地不生效这个问题排查起来特别像玄学实际上是连接状态污染。5. 避坑排查HealthArchives 从开发到交付最常见的 5 个翻车点5.1 现象MySQL 驱动报Public Key Retrieval is not allowed这个问题在 JDBC 连接 MySQL 8 时出现频率极高。现象是第一次运行程序控制台抛java.sql.SQLException: Public Key Retrieval is not allowed。原因MySQL 8 默认认证插件是caching_sha2_password客户端第一次连接时需要向服务端获取 RSA 公钥来加密密码JDBC 驱动出于安全考虑默认禁了这个动作。解决在 JDBC URL 里加参数allowPublicKeyRetrievaltrue这在前面的DBUtil工具类里已经写进去了。注意这个参数和useSSLfalse是配套的——如果用 SSL 传输公钥就不用 open retrieval但开发环境一般关了 SSL 图省事就必须放开这个开关。另外建议在 MySQL 端把连接用户的插件改成mysql_native_passwordALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 123456;一边治标一边治本。5.2 现象中文显示问号或乱码把数据库表 DROP 重建还是一样这是字符集问题而且最迷惑人的是——Java 代码和数据库字符集都已经改了 utf8mb4但数据插进去还是问号。原因通常有两个一是 MySQL 服务端character_set_server仍然默认 latin1导致建表时DEFAULT CHARSETutf8mb4没生效二是连接参数里的characterEncodingutf8mb4写错了Java 驱动支持的是characterEncodingutf8mb4较新版本或characterEncodingUTF-8两个写法不能混。排查步骤应该是层层查先SHOW VARIABLES LIKE character_set_%;看服务端全局字符集再SHOW CREATE TABLE student;看表字符集最后在 Java 里SELECT character_set_client;看连接字符集。检查发现问题是服务端全局是 latin1解决方法是改配置文件my.ini的[mysqld]段加上character-set-serverutf8mb4重启 MySQL 服务再重建表。5.3 现象Swing 界面点击按钮后白框Windows 提示“未响应”这属于 Swing 单线程模型的原生坑。现象是点击“导入 Excel”后整个窗口冻结过一会儿恢复或者永久卡住。原因按钮点击事件里直接执行了 JDBC 大批量查询或 Excel 解析这些都跑在 EDT 线程上UI 刷新被阻塞。解决按照 3.2 节的方式用SwingWorker把耗时任务扔到后台线程done()里再刷新表格。还有一个辅助做法是导入期间用JProgressBar显示进度配合SwingWorker的publish/process机制实时更新。这里多提一句setVisible(false)再setVisible(true)并不会修复 EDT 阻塞问题有人用这种“闪烁”方式假装界面刷新了治标不治本且看起来像 bug。5.4 现象数据量到一定规模后按学院查列表一次比一次慢没加索引时SELECT * FROM health_record WHERE student_id ?是全表扫描几千行时感觉不明显几万行就开始卡。原因很简单health_record表的外键列如果没有建索引MySQL 每次查询都要扫全表。解决建表时已经加了KEY idx_student (student_id)和KEY idx_check_date (check_date)。如果已经建完的表漏了索引用ALTER TABLE health_record ADD INDEX idx_student(student_id);补上。用EXPLAIN SELECT * FROM health_record WHERE student_id2025010101;查看type列从ALL全表变成ref索引查找就说明生效了。健康档案系统的索引不需要过多联合索引student_id, check_date就够了因为主要查询场景是“某个学生的历次体检记录”多留索引只会拖慢写入速度。5.5 现象java.sql.SQLFeatureNotSupportedException或驱动类找不到课设项目里最常见的两个 MySQL 驱动问题。一个是Class.forName(com.mysql.jdbc.Driver)在 MySQL 8 的驱动包mysql-connector-java8.x里已经把类移动了com.mysql.cj.jdbc.Driver老写法直接报ClassNotFoundException另一个是 MySQL 5.7 项目用了 8.x 驱动但没下载正确版本jar 包缺失。解决新项目直接用 8.x 驱动代码里可以不写Class.forName——JDBC 4.0 以后驱动包通过 SPI 自动注册DriverManager.getConnection能自动发现驱动。但为了兼容老环境保留一行Class.forName(com.mysql.cj.jdbc.Driver)也不亏。jar 包版本建议用mysql-connector-java-8.0.28.jar或更高兼容 MySQL 5.7 和 8.0 两个版本不要用 5.x 老驱动连 MySQL 8会报认证协议不支持。6. 把 HealthArchives 做到“答辩能说清、验收能演示”的进阶验证技巧功能 CRUD 做完只能算 60 分真正拉开差距的是统计分析和导出能力。校医室关心的是这个班近视率多高全校 BMI 偏瘦/超重比例怎么分布按学院对比哪个学院肺活量异常人数最多这类需求用 SQL 聚合直接输出到表格比在 Java 里写循环统计性能高一个量级。我建议加一个“统计报表”页签用三个JTable或一个JTabbedPane展示-- 按学院统计 BMI 分布用 CASE WHEN 转成可读标签 SELECT student.college, SUM(CASE WHEN health_record.bmi 18.5 THEN 1 ELSE 0 END) AS thin_count, SUM(CASE WHEN health_record.bmi BETWEEN 18.5 AND 23.9 THEN 1 ELSE 0 END) AS normal_count, SUM(CASE WHEN health_record.bmi 24 THEN 1 ELSE 0 END) AS overweight_count, COUNT(*) AS total_records FROM health_record INNER JOIN student ON student.student_id health_record.student_id WHERE health_record.check_date (SELECT MAX(check_date) FROM health_record) GROUP BY student.college;这段 SQL 逻辑INNER JOIN关联学生和体检表拿学院信息WHERE check_date (SELECT MAX(check_date)...)是“只统计最近一次体检”的写法因为一个学生有多条记录直接GROUP BY student_id会算多次从而虚高。CASE WHEN按 BMI 标准分三档对应成人标准大学生适用如果给中小学生做系统分界值要改成BMI 百分位法不能用这套成人阈值。这里参数说明BMI 边界18.5偏瘦、18.5-23.9正常、24超重是亚洲成人标准做中学版时参考《学龄儿童青少年超重与肥胖筛查》WS/T 586-2018。接着是导出功能这属于“校医真实会用到、演示时最有画面感”的模块。Swing 导出 Excel 不需要引入 POI 依赖那么重导出 CSV 文件就够Excel 直接打开 CSV// 导出当前表格数据到 CSV public void exportTableToCsv(JTable table, File file) throws IOException { try (FileWriter writer new FileWriter(file, StandardCharsets.UTF_8, false)) { // 写入表头 TableModel model table.getModel(); for (int col 0; col model.getColumnCount(); col) { writer.write(model.getColumnName(col)); if (col model.getColumnCount() - 1) { writer.write(,); } } writer.write(\n); // 写入数据行 for (int row 0; row model.getRowCount(); row) { for (int col 0; col model.getColumnCount(); col) { Object val model.getValueAt(row, col); String cell val null ? : val.toString(); // 含逗号或换行的字段用双引号包裹 if (cell.contains(,) || cell.contains(\n) || cell.contains(\)) { cell \ cell.replace(\, \\) \; } writer.write(cell); if (col model.getColumnCount() - 1) { writer.write(,); } } writer.write(\n); } } }代码逻辑说明JTable的getModel()拿到的是DefaultTableModel直接遍历行列写文件。之所以手写 CSV 而不是String.join(,, row)是因为健康档案的“备注”字段里可能会写“对青霉素过敏, 且曾做过阑尾炎手术”这里面有中文逗号也有半角逗号不加处理导出后 Excel 会把单元格拆成两列。replace(\, \\)是 CSV 标准转义规则字段里有双引号时用两个双引号代替一个外层再包一对双引号。注意最后用\n而不是\r\nExcel 两种换行都能识别但 Windows 记事本打开记事会乱这里统一 LF 即可。FileWriter 构造里StandardCharsets.UTF_8这个参数是 Java 11 之后才有的重载Java 8 只能new FileWriter(file)用默认字符集Windows 下是 GBK。如果 Java 8 必须这样导出建议先Files.newBufferedWriter(file.toPath(), StandardCharsets.UTF_8)再包一层BufferedWriter不然 Excel 打开 CSV 中文乱码。这是我踩过的坑——代码在 Windows 跑得好好的发到校医的 Mac 上导出全是乱码最后发现问题出在没显式指定 UTF-8 编码。最后一件事写几个 JUnit 测试用例。课设里大家普遍不做自动化测试但如果用户以后要维护这个系统最怕改一个 SQL 把现有功能改挂了。我至少会测三个场景插入学生后按学号能查回来、重复学号插入必须抛异常、删除学生时关联的体检记录应该被级联删除。这三条测过了核心链路就有保障。测试时用一个独立测试库别拿真实档案数据跑测试误删了没有后悔药。这套系统我在课设和两个卫生室项目里改过不下五次最深的印象是数据模型设计比 UI 代码值钱得多。Swing 界面不好看可以忍但表结构设计错了后面每个功能都要带着错误的地基盖楼。希望这篇能帮你把 MySQL 和 Swing 的组合真正跑起来少走几趟我当年走过的弯路。本文还有配套的精品资源点击获取
返回列表