MySQL+Java Swing学生健康档案系统实战指南
2026/9/23 7:53:48 网站建设 项目流程

简介:这是一套基于MySQL与Java Swing开发的学生健康档案管理系统源码,面向计算机相关专业在校生、教师及初级开发者,解决高校学生健康数据(体检+病历)的结构化录入、查询、统计与导出需求,适用于课程设计、毕业设计及项目实训等场景。资源包共98个文件,含30个核心Java源文件、38个编译后Class文件,支撑完整MVC架构;7个XML配置与SQL建库脚本(dbstuhealth.sql)保障数据库初始化,PNG/JPG图像资源用于界面图标与ER图展示,XLS表格支持数据批量导入导出,整体压缩包仅1.27MB,轻量易部署。已有76人学习下载,资源经实测可稳定运行,包含登录注册、多条件高级查询、动态折线图数据分析、多选修改/删除、Excel导入导出等完整功能模块,配套RP原型图与数据库表设计图,便于理解系统逻辑与UI交互流程。

1. 为什么一个“学生健康档案管理系统”值得用 MySQL + Java Swing 老老实实做一遍?

不是所有课程设计都该被当成练手玩具——HealthArchives 这个项目,表面看是高校信息管理课的常规作业,实际却是检验你能否把数据库建模、事务控制、GUI 响应逻辑、数据一致性校验这四根骨头真正捏在一起的试金石。我带过三届实训班,80% 的学生在“录入体检数据后修改身高却同步错改了血压值”“批量导入时突然卡死且无法回滚”“导出 Excel 时中文全变问号”这类问题上反复翻车,根本原因不是不会写JTable,而是没搞懂 Swing 的事件分发线程(EDT)和 MySQL 的 autocommit 模式怎么咬合。这个系统不追求高并发或微服务架构,但它逼你亲手处理:MySQL 表结构里「既要有外键约束又得兼容 Swing 的 JComboBox 动态加载」、「Java 中 Date 和 MySQL DATETIME 的时区穿透陷阱」、「Swing 表单校验失败时如何精准定位到具体 JTextField 并高亮提示」。如果你正卡在 Java 课程设计选题、想补足 JDBC 实战细节、或是面试前需要一个能讲透底层交互的完整项目案例——HealthArchives 就是那个能让你把“会用 PreparedStatement”变成“知道为什么必须用 setString(1, null) 而不是 setNull(1, Types.VARCHAR)”的临界点。


2. 从零搭起 HealthArchives 的最小可行骨架:建库、建表、连驱动、跑主窗

2.1 MySQL 端:按健康档案业务逻辑设计 4 张核心表(含外键与索引)

HealthArchives 的数据流本质是「学生 → 体检记录 → 异常指标 → 复查跟踪」四级关联。我们不堆字段,只建最紧耦合的四张表,每张表都带业务强约束:

-- 创建 health_db 数据库(UTF8MB4 避免 emoji 和生僻字乱码) CREATE DATABASE IF NOT EXISTS health_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE health_db; -- 学生主表:学号为主键,姓名非空,身份证号唯一且带校验位长度约束 CREATE TABLE students ( student_id VARCHAR(12) PRIMARY KEY COMMENT '12位学号,如202300000001', name VARCHAR(20) NOT NULL COMMENT '真实姓名', id_card CHAR(18) UNIQUE NOT NULL COMMENT '18位身份证号', gender ENUM('男','女') NOT NULL, class_name VARCHAR(30) NOT NULL COMMENT '所属班级,如计算机2023级1班', enrollment_date DATE NOT NULL COMMENT '入学日期', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- 体检主表:每次体检生成一条记录,关联学生,带状态标记 CREATE TABLE physical_exams ( exam_id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id VARCHAR(12) NOT NULL, exam_date DATE NOT NULL COMMENT '体检日期', exam_type ENUM('入学体检','年度体检','复查') NOT NULL DEFAULT '年度体检', status ENUM('已完成','待复检','已归档') DEFAULT '已完成', remark TEXT COMMENT '医生备注', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (student_id) REFERENCES students(student_id) ON DELETE CASCADE ); -- 指标明细表:存储每次体检的具体数值项,用 code 区分指标类型(避免宽表冗余) CREATE TABLE exam_items ( item_id BIGINT PRIMARY KEY AUTO_INCREMENT, exam_id BIGINT NOT NULL, item_code VARCHAR(20) NOT NULL COMMENT '指标编码,如BP_SYS(收缩压)、BMI、VISION_LEFT', item_value DECIMAL(8,3) COMMENT '数值型结果,NULL表示未测', item_text VARCHAR(100) COMMENT '文本型结果,如"正常"、"左眼视力4.8"', is_abnormal TINYINT(1) DEFAULT 0 COMMENT '是否异常:0否,1是', FOREIGN KEY (exam_id) REFERENCES physical_exams(exam_id) ON DELETE CASCADE, INDEX idx_exam_code (exam_id, item_code) ); -- 复查跟踪表:仅当 status='待复检' 时触发,强制记录复查动作 CREATE TABLE follow_ups ( follow_id BIGINT PRIMARY KEY AUTO_INCREMENT, exam_id BIGINT NOT NULL, follow_date DATE NOT NULL COMMENT '复查日期', follow_result TEXT COMMENT '复查结论', handler VARCHAR(20) COMMENT '处理人(校医姓名)', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (exam_id) REFERENCES physical_exams(exam_id) ON DELETE CASCADE );

关键设计说明

  • students.id_cardCHAR(18)而非VARCHAR,因身份证长度固定,减少索引碎片;
  • physical_exams.statusENUM而非VARCHAR,既防脏数据又提升查询效率(MySQL 8.0+ ENUM 内部存为整数);
  • exam_items不建systolic_bpdiastolic_bp等宽字段,而用item_code/item_value二维结构,未来加新指标(如“骨密度T值”)无需改表结构;
  • 所有外键加ON DELETE CASCADE,保证学生删除时其体检记录自动清理,避免孤儿数据。

2.2 Java 端:用 Maven 管理依赖,确保 JDBC 驱动与 MySQL 版本严格匹配

HealthArchives 必须用mysql-connector-java8.x 驱动(对应 MySQL 5.7+),若用旧版驱动连接 MySQL 8.0+ 会报Public Key Retrieval is not allowed错误。Mavenpom.xml关键片段如下:

<dependencies> <!-- MySQL JDBC Driver: 必须用 8.0.33+,否则 SSL handshake 失败 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- 使用 HikariCP 替代老旧的 BasicDataSource,连接池性能提升 3 倍以上 --> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>5.0.1</version> </dependency> <!-- 日志用 slf4j-simple,避免 logback 冗余配置 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>2.0.9</version> </dependency> </dependencies>

血泪经验

  • 别直接下载mysql-connector-java-8.0.33.jar手动丢进lib目录!Maven 会自动处理依赖传递(如HikariCP依赖slf4j-api),手动管理极易版本冲突;
  • 若用 IntelliJ IDEA,确认 Project SDK 是 JDK 11+(Swing 在 JDK 17+ 已移除部分 API,HealthArchives 兼容 JDK 11–17);
  • HikariCP初始化时务必设置connectionTimeout=30000(30秒),避免网络抖动导致界面假死。

2.3 连接池初始化:用单例模式封装 DataSource,杜绝 Connection 泄露

Swing 应用生命周期长,Connection 必须由连接池统一管理。DBConnectionPool.java实现如下:

import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; public class DBConnectionPool { private static volatile DataSource dataSource; private DBConnectionPool() {} public static DataSource getDataSource() { if (dataSource == null) { synchronized (DBConnectionPool.class) { if (dataSource == null) { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/health_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"); config.setUsername("root"); config.setPassword("your_secure_password"); // 生产环境务必从配置文件读取 config.setDriverClassName("com.mysql.cj.jdbc.Driver"); // 关键参数:防止连接空闲超时被 MySQL kill config.setMaximumPoolSize(10); config.setMinimumIdle(2); config.setConnectionTimeout(30000); config.setIdleTimeout(600000); // 10分钟 config.setMaxLifetime(1800000); // 30分钟 dataSource = new HikariDataSource(config); } } } return dataSource; } // 提供便捷 getConnection(),但业务层仍需 try-with-resources public static Connection getConnection() throws SQLException { return getDataSource().getConnection(); } }

参数说明

  • useSSL=false:开发环境关闭 SSL(生产环境必须开);
  • serverTimezone=Asia/Shanghai:强制时区对齐,解决java.util.DateDATETIME时差问题;
  • allowPublicKeyRetrieval=true:MySQL 8.0+ 默认禁用公钥检索,此参数允许驱动自动获取 RSA 公钥;
  • setMaxLifetime(1800000):连接最长存活 30 分钟,避免 MySQLwait_timeout(默认 28800 秒)导致连接失效。

2.4 Swing 主窗启动:用SwingUtilities.invokeLater确保 EDT 安全

HealthArchives 主窗MainAppFrame.java必须在事件分发线程(EDT)中创建,否则JTable渲染异常、按钮点击无响应:

import javax.swing.*; import java.awt.*; public class MainAppFrame extends JFrame { public MainAppFrame() { setTitle("学生健康档案管理系统 HealthArchives v1.0"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLayout(new BorderLayout()); // 添加菜单栏(后续章节展开) JMenuBar menuBar = new JMenuBar(); JMenu fileMenu = new JMenu("文件"); JMenuItem exitItem = new JMenuItem("退出"); exitItem.addActionListener(e -> System.exit(0)); fileMenu.add(exitItem); menuBar.add(fileMenu); setJMenuBar(menuBar); // 添加主面板(占位) JPanel centerPanel = new JPanel(new BorderLayout()); centerPanel.add(new JLabel("HealthArchives 主界面 —— 双击左侧导航树查看功能", JLabel.CENTER), BorderLayout.CENTER); add(centerPanel, BorderLayout.CENTER); pack(); setLocationRelativeTo(null); // 居中显示 setVisible(true); } public static void main(String[] args) { // Swing 必须在 EDT 中启动 SwingUtilities.invokeLater(() -> { try { // 设置系统外观(可选,避免 Win/Linux/Mac 渲染差异) UIManager.setLookAndFeel(UIManager.getSystemLookAndFeel()); } catch (Exception e) { // 外观设置失败不影响主流程 System.err.println("UIManager set failed: " + e.getMessage()); } new MainAppFrame(); }); } }

为什么必须invokeLater
Swing 组件不是线程安全的。若在主线程直接new MainAppFrame(),窗口可能渲染不全、事件监听器注册失败。invokeLater将创建操作提交到 EDT 队列,确保所有 UI 操作串行执行。


3. 核心功能落地:学生管理模块的 CRUD 实现与 Swing 表单联动

3.1 学生信息录入窗:用 GroupLayout 精确控制表单布局,避免 GridBagLayout 玄学错位

Swing 布局中GroupLayout是最可控的方案(尤其对新手)。StudentAddDialog.java实现如下:

import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.sql.*; public class StudentAddDialog extends JDialog { private JTextField studentIdField; private JTextField nameField; private JTextField idCardField; private JComboBox<String> genderCombo; private JTextField classField; private JDateChooser enrollmentDateChooser; // 使用 jcalendar 包,见下文依赖 private JButton confirmBtn; private JButton cancelBtn; public StudentAddDialog(Frame owner) { super(owner, "新增学生", true); initUI(); pack(); setLocationRelativeTo(owner); } private void initUI() { setLayout(new BorderLayout()); // 表单面板 JPanel formPanel = new JPanel(); GroupLayout layout = new GroupLayout(formPanel); formPanel.setLayout(layout); layout.setAutoCreateGaps(true); layout.setAutoCreateContainerGaps(true); studentIdField = new JTextField(15); nameField = new JTextField(15); idCardField = new JTextField(18); genderCombo = new JComboBox<>(new String[]{"男", "女"}); classField = new JTextField(20); enrollmentDateChooser = new JDateChooser(); // 日期选择器,避免手动输格式错误 // 水平组:各控件并排对齐 layout.setHorizontalGroup(layout.createSequentialGroup() .addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING) .addComponent(new JLabel("学号:")) .addComponent(new JLabel("姓名:")) .addComponent(new JLabel("身份证号:")) .addComponent(new JLabel("性别:")) .addComponent(new JLabel("班级:")) .addComponent(new JLabel("入学日期:")) ) .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) .addComponent(studentIdField) .addComponent(nameField) .addComponent(idCardField) .addComponent(genderCombo) .addComponent(classField) .addComponent(enrollmentDateChooser) ) ); // 垂直组:控件垂直排列 layout.setVerticalGroup(layout.createSequentialGroup() .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) .addComponent(new JLabel("学号:")) .addComponent(studentIdField) ) .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) .addComponent(new JLabel("姓名:")) .addComponent(nameField) ) .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) .addComponent(new JLabel("身份证号:")) .addComponent(idCardField) ) .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) .addComponent(new JLabel("性别:")) .addComponent(genderCombo) ) .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) .addComponent(new JLabel("班级:")) .addComponent(classField) ) .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) .addComponent(new JLabel("入学日期:")) .addComponent(enrollmentDateChooser) ) ); // 按钮面板 JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); confirmBtn = new JButton("确定"); cancelBtn = new JButton("取消"); buttonPanel.add(confirmBtn); buttonPanel.add(cancelBtn); add(formPanel, BorderLayout.CENTER); add(buttonPanel, BorderLayout.SOUTH); // 事件绑定 confirmBtn.addActionListener(new AddStudentActionListener()); cancelBtn.addActionListener(e -> dispose()); } private class AddStudentActionListener implements ActionListener { @Override public void actionPerformed(ActionEvent e) { if (validateForm()) { try (Connection conn = DBConnectionPool.getConnection(); PreparedStatement ps = conn.prepareStatement( "INSERT INTO students (student_id, name, id_card, gender, class_name, enrollment_date) VALUES (?, ?, ?, ?, ?, ?)")) { ps.setString(1, studentIdField.getText().trim()); ps.setString(2, nameField.getText().trim()); ps.setString(3, idCardField.getText().trim()); ps.setString(4, (String) genderCombo.getSelectedItem()); ps.setString(5, classField.getText().trim()); ps.setDate(6, new java.sql.Date(enrollmentDateChooser.getDate().getTime())); int rows = ps.executeUpdate(); if (rows > 0) { JOptionPane.showMessageDialog(StudentAddDialog.this, "学生添加成功!"); dispose(); } } catch (SQLException ex) { JOptionPane.showMessageDialog(StudentAddDialog.this, "数据库错误:" + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); } } } } private boolean validateForm() { if (studentIdField.getText().trim().isEmpty()) { JOptionPane.showMessageDialog(this, "学号不能为空!", "输入错误", JOptionPane.WARNING_MESSAGE); studentIdField.requestFocus(); return false; } if (nameField.getText().trim().isEmpty()) { JOptionPane.showMessageDialog(this, "姓名不能为空!", "输入错误", JOptionPane.WARNING_MESSAGE); nameField.requestFocus(); return false; } // 身份证号基础校验(18位数字+X) String idCard = idCardField.getText().trim(); if (!idCard.matches("\\d{17}[\\dXx]")) { JOptionPane.showMessageDialog(this, "身份证号格式错误!应为18位数字或末位X", "输入错误", JOptionPane.WARNING_MESSAGE); idCardField.requestFocus(); return false; } return true; } }

关键点说明

  • JDateChooser需额外引入jcalendar依赖(Maven):
    <dependency> <groupId>com.toedter</groupId> <artifactId>jcalendar</artifactId> <version>1.4</version> </dependency>
  • validateForm()idCard.matches("\\d{17}[\\dXx]")仅做长度和字符校验,真实项目应集成国标 GB11643-1999 校验算法;
  • ps.setDate(6, ...)java.sql.Date而非java.util.Date,避免时区转换错误;
  • try-with-resources自动关闭PreparedStatement,但Connection由连接池管理,不在此处 close。

3.2 学生列表展示:用DefaultTableModel动态刷新 JTable,避免手动fireTableDataChanged()

StudentListPanel.java实现表格数据加载与实时更新:

import javax.swing.*; import javax.swing.table.DefaultTableModel; import java.awt.*; import java.sql.*; public class StudentListPanel extends JPanel { private JTable table; private DefaultTableModel tableModel; private JButton refreshBtn; private JButton addBtn; public StudentListPanel() { initUI(); loadStudents(); } private void initUI() { setLayout(new BorderLayout()); // 表格模型:列名与数据类型预设 String[] columnNames = {"学号", "姓名", "身份证号", "性别", "班级", "入学日期"}; tableModel = new DefaultTableModel(columnNames, 0) { @Override public Class<?> getColumnClass(int columnIndex) { // 指定列类型,让 JTable 正确渲染(如日期列右对齐) if (columnIndex == 5) return java.util.Date.class; return String.class; } }; table = new JTable(tableModel); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); JScrollPane scrollPane = new JScrollPane(table); add(scrollPane, BorderLayout.CENTER); // 工具栏 JPanel toolbar = new JPanel(new FlowLayout(FlowLayout.LEFT)); refreshBtn = new JButton("刷新"); addBtn = new JButton("新增"); toolbar.add(refreshBtn); toolbar.add(addBtn); add(toolbar, BorderLayout.SOUTH); // 事件绑定 refreshBtn.addActionListener(e -> loadStudents()); addBtn.addActionListener(e -> { StudentAddDialog dialog = new StudentAddDialog(SwingUtilities.getWindowAncestor(this)); dialog.setVisible(true); }); } public void loadStudents() { // 清空现有数据 tableModel.setRowCount(0); String sql = "SELECT student_id, name, id_card, gender, class_name, enrollment_date FROM students ORDER BY enrollment_date DESC"; try (Connection conn = DBConnectionPool.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { Object[] row = { rs.getString("student_id"), rs.getString("name"), rs.getString("id_card"), rs.getString("gender"), rs.getString("class_name"), rs.getDate("enrollment_date") // java.sql.Date 可直接塞入 tableModel }; tableModel.addRow(row); } } catch (SQLException ex) { JOptionPane.showMessageDialog(this, "加载学生列表失败:" + ex.getMessage(), "数据库错误", JOptionPane.ERROR_MESSAGE); } } }

为什么用DefaultTableModel而非AbstractTableModel

  • DefaultTableModel内置addRow()setRowCount(0)等方法,适合 CRUD 场景的快速增删;
  • getColumnClass()返回java.util.Date.class后,JTable 自动对日期列右对齐、调用DateFormat渲染,无需重写TableCellRenderer
  • loadStudents()每次清空再重载,逻辑清晰,避免fireTableRowsInserted()等手动通知的遗漏风险。

3.3 学生信息编辑:双击表格行弹出编辑窗,用ResultSet.CONCUR_UPDATABLE实现原地更新

StudentEditDialog.java支持双击编辑,关键在于PreparedStatementCONCUR_UPDATABLE模式:

import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.sql.*; public class StudentEditDialog extends JDialog { private JTextField studentIdField; private JTextField nameField; private JTextField idCardField; private JComboBox<String> genderCombo; private JTextField classField; private JDateChooser enrollmentDateChooser; private JButton confirmBtn; private JButton cancelBtn; private String originalStudentId; // 用于 WHERE 条件,防止学号被改 public StudentEditDialog(Frame owner, String studentId) { super(owner, "编辑学生信息", true); this.originalStudentId = studentId; initUI(); loadStudentData(); pack(); setLocationRelativeTo(owner); } private void initUI() { // 布局同 StudentAddDialog,省略重复代码... // ...(此处省略布局代码,结构完全一致) confirmBtn.addActionListener(new UpdateStudentActionListener()); cancelBtn.addActionListener(e -> dispose()); } private void loadStudentData() { String sql = "SELECT * FROM students WHERE student_id = ?"; try (Connection conn = DBConnectionPool.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, originalStudentId); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { studentIdField.setText(rs.getString("student_id")); nameField.setText(rs.getString("name")); idCardField.setText(rs.getString("id_card")); genderCombo.setSelectedItem(rs.getString("gender")); classField.setText(rs.getString("class_name")); enrollmentDateChooser.setDate(rs.getDate("enrollment_date")); } } } catch (SQLException ex) { JOptionPane.showMessageDialog(this, "加载学生数据失败:" + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); } } private class UpdateStudentActionListener implements ActionListener { @Override public void actionPerformed(ActionEvent e) { if (validateForm()) { String sql = "UPDATE students SET name=?, id_card=?, gender=?, class_name=?, enrollment_date=? WHERE student_id=?"; try (Connection conn = DBConnectionPool.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, nameField.getText().trim()); ps.setString(2, idCardField.getText().trim()); ps.setString(3, (String) genderCombo.getSelectedItem()); ps.setString(4, classField.getText().trim()); ps.setDate(5, new java.sql.Date(enrollmentDateChooser.getDate().getTime())); ps.setString(6, originalStudentId); // 用原始学号更新,防止用户改学号 int rows = ps.executeUpdate(); if (rows > 0) { JOptionPane.showMessageDialog(StudentEditDialog.this, "更新成功!"); dispose(); } } catch (SQLException ex) { JOptionPane.showMessageDialog(StudentEditDialog.this, "更新失败:" + ex.getMessage(), "数据库错误", JOptionPane.ERROR_MESSAGE); } } } } private boolean validateForm() { // 同 StudentAddDialog.validateForm(),省略 return true; } }

关键设计点

  • WHERE student_id=?中的参数用originalStudentId(构造函数传入),而非studentIdField.getText(),防止用户恶意修改学号导致数据错乱;
  • 更新语句不更新student_id字段,学号作为主键应 immutable;
  • loadStudentData()executeQuery()而非executeUpdate(),符合只读场景。

4. 避坑:HealthArchives 开发中 5 个高频翻车点与硬核解法

4.1 现象:Swing 界面卡死,鼠标悬停按钮变沙漏,但 CPU 占用率极低

原因:在 EDT 线程中执行耗时的 JDBC 操作(如ps.executeQuery()),阻塞了整个 UI 事件队列。Swing 的 repaint、click 等事件全部积压。
解决:所有数据库操作必须移出 EDT。使用SwingWorker异步执行:

private void loadStudentsAsync() { new SwingWorker<Void, Void>() { @Override protected Void doInBackground() throws Exception { // 此方法在后台线程运行 loadStudentsFromDB(); // 原来的 loadStudents() 方法体 return null; } @Override protected void done() { // 此方法在 EDT 中运行,可安全更新 UI try { get(); // 检查是否有异常 } catch (Exception ex) { JOptionPane.showMessageDialog(StudentListPanel.this, "加载失败:" + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); } } }.execute(); }

注意SwingWorker是 Swing 官方推荐的异步方案,比Thread+SwingUtilities.invokeLater更安全,自动处理线程切换。

4.2 现象:MySQL 中存的日期是2023-09-01,Java 读出来却变成2023-08-31

原因:JDBC 驱动未指定serverTimezone,MySQL 服务器时区(如SYSTEM)与 JVM 时区(如Asia/Shanghai)不一致,导致java.sql.Date解析偏移。
解决:JDBC URL 中强制声明serverTimezone=Asia/Shanghai,且确保 MySQL 服务器时区已设为+08:00

SET GLOBAL time_zone = '+08:00'; SET time_zone = '+08:00';

4.3 现象:JTable显示中文全是方块(□□□),导出 Excel 也是乱码

原因:MySQL 数据库、表、字段的字符集不是utf8mb4,或 JDBC URL 缺少characterEncoding=utf8mb4
解决

  1. 创建库时指定字符集:CREATE DATABASE health_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
  2. JDBC URL 加参数:jdbc:mysql://localhost:3306/health_db?characterEncoding=utf8mb4&...
  3. 确认 MySQL 配置文件my.cnf[client][mysqld]段落均设default-character-set=utf8mb4

4.4 现象:新增学生后,列表没刷新,必须手动点“刷新”按钮才看到新数据

原因StudentAddDialog中插入成功后,未通知StudentListPanel更新表格。Swing 组件间无自动绑定。
解决:采用观察者模式,在StudentAddDialog中定义回调接口:

// 在 StudentAddDialog 中 public interface OnStudentAddedListener { void onStudentAdded(); } private OnStudentAddedListener listener; public void setOnStudentAddedListener(OnStudentAddedListener listener) { this.listener = listener; } // 插入成功后调用 if (rows > 0) { JOptionPane.showMessageDialog(...); if (listener != null) listener.onStudentAdded(); dispose(); }

StudentListPanel创建对话框时传入监听器:

addBtn.addActionListener(e -> { StudentAddDialog dialog = new StudentAddDialog(...); dialog.setOnStudentAddedListener(this::loadStudents); // 直接绑定刷新方法 dialog.setVisible(true); });

4.5 现象:JComboBox下拉选项从数据库加载后,选中项无法通过getSelectedItem()获取字符串

原因JComboBox默认使用Object.toString(),若从ResultSet直接addItem(rs.getObject("class_name")),可能存入的是String也可能存入nullgetSelectedItem()返回null
解决:显式调用getString()并判空:

while (rs.next()) { String className = rs.getString("class_name"); if (className != null && !className.trim().isEmpty()) { classCombo.addItem(className.trim()); } }

并在actionPerformed中用String selected = (String) combo.getSelectedItem();强制转型。


5. 体检记录与异常指标联动:用 MySQL 视图 + Swing Tree 实现多维数据钻取

5.1 构建体检视图:聚合学生、体检、指标三表,预计算异常状态

HealthArchives 的核心价值在于“一眼看出谁需要复查”。我们用 MySQL 视图将分散数据聚合成可直接查询的维度:

-- 创建视图:学生基本信息 + 最近一次体检状态 + 异常指标数 CREATE VIEW student_exam_summary AS SELECT s.student_id, s.name, s.class_name, s.gender, MAX(e.exam_date) AS last_exam_date, COUNT(CASE WHEN i.is_abnormal = 1 THEN 1 END) AS abnormal_count, GROUP_CONCAT(DISTINCT i.item_code ORDER BY i.item_code SEPARATOR ', ') AS abnormal_items, MAX(e.status) AS current_status FROM students s LEFT JOIN physical_exams e ON s.student_id = e.student_id LEFT JOIN exam_items i ON e.exam_id = i.exam_id AND i.is_abnormal = 1 GROUP BY s.student_id, s.name, s.class_name, s.gender;

视图优势

  • 查询SELECT * FROM student_exam_summary WHERE abnormal_count > 0即得全部待复检学生;
  • GROUP_CONCAT将多个异常指标合并为逗号分隔字符串,Swing 中可直接显示;
  • LEFT JOIN保证未体检学生也出现在列表中(last_exam_date=NULL)。

5.2 Swing 导航树实现:用 JTree 展示「学生 → 体检 → 指标」三级钻取

ExamTreePanel.java构建可展开的树形结构:

import javax.swing.*; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.TreeSelectionModel; import java.awt.*; import java.sql.*; public class ExamTreePanel extends JPanel { private JTree tree; private DefaultMutableTreeNode root; public ExamTreePanel() { setLayout(new BorderLayout()); root = new DefaultMutableTreeNode("健康档案树"); tree = new JTree(root); tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); tree.addTreeSelectionListener(e -> onTreeSelection()); JScrollPane scrollPane = new JScrollPane(tree); add(scrollPane, BorderLayout.CENTER); loadTreeData(); } private void loadTreeData() { root.removeAllChildren(); // 第一层:学生节点 String sql = "SELECT student_id, name, class_name FROM students ORDER BY class_name, student_id"; try (Connection conn = DBConnectionPool.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { String studentId = rs.getString("student_id"); String name = rs.getString("name"); String className = rs.getString("class_name"); DefaultMutableTreeNode studentNode = new DefaultMutableTreeNode( String.format("[%s] %s", studentId, name)); root.add(studentNode); // 第二层:该学生的体检记录 String examSql = "SELECT exam_id, exam_date, exam_type, status FROM physical_exams WHERE student_id = ? ORDER BY exam_date DESC"; try (PreparedStatement examPs = conn.prepareStatement(examSql)) { examPs.setString(1, studentId); try (ResultSet examRs = examPs.executeQuery()) { while (examRs.next()) { long examId = examRs.getLong("exam_id"); String examDate = examRs.getDate("exam_date").toString(); String examType = examRs.getString("exam_type"); String status = examRs.getString("status"); DefaultMutableTreeNode examNode = new DefaultMutableTreeNode( String.format("%s %s (%s)", examDate, examType, status)); studentNode.add(examNode); // 第三层:该体检的异常指标 String itemSql = "SELECT item_code, item_value, item_text FROM exam_items WHERE exam_id = ? AND is_abnormal = 1"; try (PreparedStatement itemPs = conn.prepareStatement(itemSql)) { itemPs.setLong(1, examId); try (ResultSet itemRs = itemPs.executeQuery()) { while (itemRs.next()) { String code = itemRs.getString("item_code"); String value = itemRs.getString("item_value"); String text = itemRs.getString("item_text"); String display = String.format("%s: %s%s", code, value != null ? value : "", text != null ? " (" + text + ")" : ""); examNode.add(new DefaultMutableTreeNode(display)); } } } } } } } } catch (SQLException ex) { JOptionPane.showMessageDialog(this, "加载树形数据失败:" + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); } // 刷新树 ((DefaultTreeModel) tree.getModel()).reload(); } private void onTreeSelection() { DefaultMutableTreeNode selected = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent(); if (selected == null || selected.isRoot()) return; Object userObj = selected.getUserObject(); if (userObj instanceof String) { String nodeText = (String) userObj; // 解析节点文本,提取 student_id 或 exam_id if (nodeText.contains("[")) { // 学生节点:提取学号 String studentId = nodeText.substring(nodeText.indexOf("[") + 1 <p> <a href="https://download.csdn.net/download/m0_46013789/89590607" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询