
简介这是一套基于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_card用CHAR(18)而非VARCHAR因身份证长度固定减少索引碎片physical_exams.status用ENUM而非VARCHAR既防脏数据又提升查询效率MySQL 8.0 ENUM 内部存为整数exam_items不建systolic_bp、diastolic_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 groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- 使用 HikariCP 替代老旧的 BasicDataSource连接池性能提升 3 倍以上 -- dependency groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId version5.0.1/version /dependency !-- 日志用 slf4j-simple避免 logback 冗余配置 -- dependency groupIdorg.slf4j/groupId artifactIdslf4j-simple/artifactId version2.0.9/version /dependency /dependencies血泪经验别直接下载mysql-connector-java-8.0.33.jar手动丢进lib目录Maven 会自动处理依赖传递如HikariCP依赖slf4j-api手动管理极易版本冲突若用 IntelliJ IDEA确认 Project SDK 是 JDK 11Swing 在 JDK 17 已移除部分 APIHealthArchives 兼容 JDK 11–17HikariCP初始化时务必设置connectionTimeout3000030秒避免网络抖动导致界面假死。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?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue); 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(); } }参数说明useSSLfalse开发环境关闭 SSL生产环境必须开serverTimezoneAsia/Shanghai强制时区对齐解决java.util.Date与DATETIME时差问题allowPublicKeyRetrievaltrueMySQL 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(); }); } }为什么必须invokeLaterSwing 组件不是线程安全的。若在主线程直接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 JComboBoxString 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依赖Mavendependency groupIdcom.toedter/groupId artifactIdjcalendar/artifactId version1.4/version /dependencyvalidateForm()中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而非AbstractTableModelDefaultTableModel内置addRow()、setRowCount(0)等方法适合 CRUD 场景的快速增删getColumnClass()返回java.util.Date.class后JTable 自动对日期列右对齐、调用DateFormat渲染无需重写TableCellRendererloadStudents()每次清空再重载逻辑清晰避免fireTableRowsInserted()等手动通知的遗漏风险。3.3 学生信息编辑双击表格行弹出编辑窗用ResultSet.CONCUR_UPDATABLE实现原地更新StudentEditDialog.java支持双击编辑关键在于PreparedStatement的CONCUR_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 JComboBoxString 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字段学号作为主键应 immutableloadStudentData()用executeQuery()而非executeUpdate()符合只读场景。4. 避坑HealthArchives 开发中 5 个高频翻车点与硬核解法4.1 现象Swing 界面卡死鼠标悬停按钮变沙漏但 CPU 占用率极低原因在 EDT 线程中执行耗时的 JDBC 操作如ps.executeQuery()阻塞了整个 UI 事件队列。Swing 的 repaint、click 等事件全部积压。解决所有数据库操作必须移出 EDT。使用SwingWorker异步执行private void loadStudentsAsync() { new SwingWorkerVoid, 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 官方推荐的异步方案比ThreadSwingUtilities.invokeLater更安全自动处理线程切换。4.2 现象MySQL 中存的日期是2023-09-01Java 读出来却变成2023-08-31原因JDBC 驱动未指定serverTimezoneMySQL 服务器时区如SYSTEM与 JVM 时区如Asia/Shanghai不一致导致java.sql.Date解析偏移。解决JDBC URL 中强制声明serverTimezoneAsia/Shanghai且确保 MySQL 服务器时区已设为08:00SET GLOBAL time_zone 08:00; SET time_zone 08:00;4.3 现象JTable显示中文全是方块□□□导出 Excel 也是乱码原因MySQL 数据库、表、字段的字符集不是utf8mb4或 JDBC URL 缺少characterEncodingutf8mb4。解决创建库时指定字符集CREATE DATABASE health_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;JDBC URL 加参数jdbc:mysql://localhost:3306/health_db?characterEncodingutf8mb4...确认 MySQL 配置文件my.cnf中[client]和[mysqld]段落均设default-character-setutf8mb4。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_dateNULL。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 hrefhttps://download.csdn.net/download/m0_46013789/89590607 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p