ARTICLE DETAIL

资讯详情

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

基于Java Swing和MySQL的学生健康档案管理系统开发实战

基于Java Swing和MySQL的学生健康档案管理系统开发实战 简介这份基于MySQL与Java Swing的学生健康档案管理系统项目专为计算机相关专业学生、教师及企业开发人员打造可无缝用于毕业设计、课程设计或项目初期演示。系统自上而下分解为学生信息管理、体检信息管理和病历信息管理完整覆盖学号、姓名、性别、系别、诊断、年龄、身高、体重、胸围、日期等核心字段并实现数据新增、修改、删除、多选项高级查询、Excel导入导出等多类操作能够支撑日常健康档案维护全流程。资源共包含98个文件以Java源码和class字节码为主配合SQL建表脚本、XML配置、properties配置文件、Excel示例数据、PNG/JPG设计图含E-R图、数据库表结构、界面效果图以及Rational Rose设计文档整体仅1.27MB目录规划分明导入IDE后即可运行调试。目前已有76人学习下载。该源码在提交前已通过多轮测试曾获导师认可并在答辩中取得96分具备较高参考价值从界面控件布局到数据库表关系都有完整呈现方便快速厘清实现思路也便于在此基础上升级或重构尤其适合Swing与MySQL综合应用的学习进阶。1. 学生健康档案管理系统 HealthArchives为什么这个组合在课程设计和校医室场景里最省事学校校医室要管的是一个年级几百名学生的身高、体重、视力、血压和过敏史录入的人是校医使用环境是局域网内的几台 Windows 机器。这种场景下基于 MySQL Java swing 实现的学生健康档案管理系统 HealthArchives 正好命中MySQL 负责按学号、班级、体检日期把数据存成结构化记录Swing 提供一个不用装 Tomcat、不用配前端工程的桌面窗口JDBC 把两层接起来。相比 Web 方案它少了一整套前后端工程和服务器部署却把最关键的增删改查、模糊查询、导出这些操作都覆盖了这也是它常被用作 Java 课程设计和校内小型工具的原因。适合的人群也很明确做 Java 课程设计或毕业设计的学生需要一个能现场演示、代码能讲清楚的完整案例校内系统的开发维护者比起维护一个 Web 站点更愿意要一个双击能跑的桌面程序。如果你已经遇到过“界面写完了却连不上数据库”“表格刷新总是带着旧数据”这两类问题下面就是按落地顺序把这些坑提前标出来的完整过程。2. 先立数据模型健康档案数据库的三张表设计与字段边界写任何 Java 界面之前先把 MySQL 里的表立起来。HealthArchives 这类管理系统最怕的不是界面写不出来而是表字段一开始就设计错后面 Java 代码跟着推倒重来。常见做法是把数据拆成学生基本信息、体检记录、系统登录用户三块而不是把所有字段揉进一张大表里硬撑。2.1 拆实体学生、体检记录、登录用户之间的关系一个学生有且仅有一条基本信息学号、姓名、性别、出生日期、班级、年级、联系电话、过敏史。而体检记录是多次的一年级入学体检、每学期常规体检、体质测试一个学生会有多条记录。所以 student 和 physical_exam 是一对多关系student_id 在 physical_exam 里做外键。登录用户表单独存放账号、密码、角色和业务数据分开方便以后扩展管理员、校医两种权限。我见过不少课程设计把体检项目做成横向字段height_1、weight_1、vision_left_1 这种重复列理由是“这样查起来直观”。这种设计第一次演示没问题第二次补录时就必须改表结构新增一列、改一遍 Java。正确思路是体检项作为行存一次体检一行exam_date 是每条记录的时间坐标。这样“某学生历年身高变化”只是一条带排序的 SQL而不是改表结构。字段边界上还有两点要注意一是不要存冗余汇总比如直接在 student 表里放 last_height除非确有性能要求否则每次更新体检记录都要连带改学生表事务边界变大二是把“体检日期”独立成列而不要靠 create_time 判断因为 create_time 是录入时间不代表体检发生时间这两个时间在批量补录时完全不相等。2.2 建库建表给新手的 HealthArchives SQL 脚本我用 MySQL 8.0字符集统一 utf8mb4。库名 health_archives脚本用 Navicat for MySQL 或 MySQL Workbench 的查询窗口执行都行。下面是一份能直接跑的建表脚本CREATE DATABASE IF NOT EXISTS health_archives DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE health_archives; -- 学生基本信息表 CREATE TABLE student ( student_id VARCHAR(20) NOT NULL COMMENT 学号业务主键, full_name VARCHAR(30) NOT NULL COMMENT 姓名, gender CHAR(1) NOT NULL DEFAULT 男 COMMENT 性别, birth_date DATE NULL COMMENT 出生日期, class_name VARCHAR(30) NOT NULL COMMENT 班级如 高二(3)班, grade VARCHAR(10) NULL COMMENT 年级, phone VARCHAR(20) NULL COMMENT 联系电话, allergy VARCHAR(200) NULL COMMENT 过敏史或既往病史, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (student_id) ) ENGINE InnoDB COMMENT 学生基本信息; -- 体检记录表 CREATE TABLE physical_exam ( record_id INT NOT NULL AUTO_INCREMENT COMMENT 记录主键, student_id VARCHAR(20) NOT NULL COMMENT 学号, exam_date DATE NOT NULL COMMENT 体检日期, height_cm DECIMAL(5,1) NOT NULL DEFAULT 0.0 COMMENT 身高cm, weight_kg DECIMAL(5,2) NOT NULL DEFAULT 0.0 COMMENT 体重kg, vision_left DECIMAL(3,1) NULL COMMENT 左眼视力, vision_right DECIMAL(3,1) NULL COMMENT 右眼视力, systolic INT NULL COMMENT 收缩压mmHg, diastolic INT NULL COMMENT 舒张压mmHg, note VARCHAR(500) NULL COMMENT 异常项备注, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (record_id), KEY idx_student_date (student_id, exam_date), CONSTRAINT fk_exam_student FOREIGN KEY (student_id) REFERENCES student (student_id) ) ENGINE InnoDB COMMENT 学生体检记录; -- 登录用户表 CREATE TABLE sys_user ( user_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, username VARCHAR(30) NOT NULL UNIQUE COMMENT 登录账号, password VARCHAR(64) NOT NULL COMMENT 密码建议存 SHA-256 摘要, role VARCHAR(10) NOT NULL DEFAULT admin COMMENT 角色, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE InnoDB COMMENT 系统登录用户;脚本里的三个设计点student_id 用 VARCHAR(20) 而不是 INT因为很多学校的学号是“G2024001”这种带前缀的字符串用 INT 会在 Java 侧多一层转换physical_exam 主键用自增 record_id是因为同一学生在同一天可能存在多条补录记录用 student_id exam_date 做业务主键不稳身高体重用 DECIMAL 而不是 FLOAT避免 170.1 这种值在浮点运算里出现 170.099999 的显示问题。外键写在建表语句里比 Java 代码里自己判断更省事插入非法学号时 MySQL 直接报错。2.3 字段类型和默认值delete_flag 默认 0 的取舍MySQL 相关讨论里经常见到“设置默认值为0”的写法最常见的是软删除字段ALTER TABLE student ADD COLUMN delete_flag TINYINT NOT NULL DEFAULT 0 COMMENT 0 正常1 删除;如果你做的是要给长期使用的系统软删除有它的价值误删能恢复。但我的实践结论是课程设计和校内小系统不要一上来就加 delete_flag。原因是它会让每一条查询 SQL 都变成 WHERE delete_flag 0Java 代码里忘写一次就会出现“已删除数据出现在列表里”的怪问题排查起来非常痛苦。宁可做物理删除把数据备份交给 mysqldump也不要让业务代码每一行都背着软删除条件。身高、体重这类数值字段的 DEFAULT 0 也有讲究录入界面允许为空时0 和空是两种语义。0 表示“这次体检没测”空表示“没记录”。用 NOT NULL DEFAULT 0.0 配合查询时 WHERE height_cm 0 判断比允许 NULL 再每次判断 IS NULL 更容易写 SQL。这是我用过两种写法之后的明确偏好短时间内看不出差别改到第三个查询就体会到哪种省心。3. 连通 MySQLJDBC 连接串、配置文件与连接池选型表建好之后第二步不是画界面而是先让 Java 能连上 MySQL。这里最容易踩坑老教程让你写 com.mysql.jdbc.DriverMySQL 8 驱动里类名已经变成 com.mysql.cj.jdbc.Driver连接串不带时区参数MySQL 8 直接抛异常。先把 JDBC 这一层打通后面的 Swing 界面才有数据可显示。3.1 驱动和 URL 参数Cannot load driver 报错大多出在这里我一般用 Maven 托管依赖坐标 mysql-connector-j版本 8.0 系列都很稳。如果不熟悉 Maven去 MySQL 官网下载对应版本的驱动 jar手动导入项目 lib 目录也可以。核心是类名和 URL 参数别写错。最小可跑代码String url jdbc:mysql://127.0.0.1:3306/health_archives ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; String user root; String password 123456; Class.forName(com.mysql.cj.jdbc.Driver); try (Connection conn DriverManager.getConnection(url, user, password)) { System.out.println(连接成功当前库 conn.getCatalog()); } catch (ClassNotFoundException | SQLException e) { e.printStackTrace(); }逻辑说明Class.forName 这一步在 JDBC 4 之后其实可以省略但保留它有好处——如果驱动 jar 没导对它会第一时间抛出 ClassNotFoundException方便你判断问题是“缺包”而不是“连不上”。URL 里三个参数是 MySQL 8 环境下的标配逐个解释URL 参数作用不设会怎样serverTimezoneAsia/Shanghai指定客户端时区与服务器时区对齐报 The server time zone value ??? 连接被拒useSSLfalse关闭 SSL 握手本地开发偶发握手超时、链接失败characterEncodingutf8指定传输字符集中文写入数据库变成问号提示MySQL 8 默认时区经常是系统时区JDBC 驱动拿不到合法时区就直接中止连接。把 serverTimezone 显式写在 URL 里就行不要改 MySQL 全局 time_zone那个会影响服务器上其他业务。3.2 把连接信息放到 db.properties换库不重新编译直接写在 Java 类里的连接串当然能跑但有个实际问题课程设计拷贝给老师演示时老师的 MySQL 密码和你本地不一样每次改密码都得重新编译整个项目。常见做法是把连接信息抽到 src 根目录的 db.propertiesjdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://127.0.0.1:3306/health_archives?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 jdbc.userroot jdbc.password123456对应的加载工具类public class DbUtil { private static String url; private static String user; private static String password; static { try (InputStream in DbUtil.class.getResourceAsStream(/db.properties)) { Properties prop new Properties(); prop.load(in); url prop.getProperty(jdbc.url); user prop.getProperty(jdbc.user); password prop.getProperty(jdbc.password); Class.forName(prop.getProperty(jdbc.driver)); } catch (Exception e) { throw new ExceptionInInitializerError(读取 db.properties 失败: e.getMessage()); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } }注意 getResourceAsStream 的路径以 / 开头文件在源码根目录时打包后会在 classpath 根路径下这个写法能读到。如果打包后把 db.properties 放在 jar 外面需要改成 FileInputStream 按相对路径加载这一点后面的打包章节会专门提。配置文件的好处是换库、换密码只改文本不动代码。3.3 单机桌面程序要不要连接池我的选择理由热词里总能看到“mysql 的数据库连接池”HikariCP、Druid、c3p0 各有说法。但我的实际结论是HealthArchives 这个场景默认用 DriverManager 就够了不要把引入连接池当成“专业度”指标。Swing 桌面程序是单用户、低并发一次点按钮也就开一两条连接用完即关MySQL 对这种负载毫无压力。连接池的价值在高频并发场景例如 Web 后端一秒钟几十个请求连接复用能省下握手开销桌面程序根本到不了这个量级。如果你确实想上连接池我推荐 HikariCP。依赖配好之后核心是四行代码HikariConfig config new HikariConfig(); config.setJdbcUrl(url); config.setUsername(user); config.setPassword(password); config.setMaximumPoolSize(5); config.setMinimumIdle(1); config.setConnectionTimeout(3000); HikariDataSource dataSource new HikariDataSource(config);参数说明单机 Swing 程序 maximumPoolSize 设 5 就够后台查询最多并行几条连接setConnectionTimeout(3000) 是拿不到连接时的等待上限比 MySQL 驱动默认的 30 秒更能让你感知到服务没启动。连接池适合项目里已经存在多个 DAO 类和批量操作的场景纯演示项目别折腾它多一个依赖就多一个出问题的面。4. Swing 界面层从登录窗口到健康档案增删改查数据库通了开始写界面。Swing 的定位是桌面客户端骨架JFrame 做容器JTable 做数据展示JTextField 和 JComboBox 做查询条件按钮触发 DAO 方法。HealthArchives 的界面不需要花哨稳定、能连续录入、误操作不崩就是这一章的目标。4.1 主窗口布局JFrame JTable 工具栏怎么组织主窗口用 BorderLayout 分三块NORTH 放查询条件面板CENTER 放表格SOUTH 放操作按钮。查询条件用 JComboBox 选“学号/姓名/班级”旁边一个 JTextField 输入关键字再加“查询”“刷新”两个按钮。表格放进 JScrollPane否则表头显示不出来。JFrame frame new JFrame(学生健康档案管理系统 HealthArchives); frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); frame.setSize(1000, 640); frame.setLocationRelativeTo(null); JPanel queryPanel new JPanel(new FlowLayout(FlowLayout.LEFT)); JComboBoxString fieldBox new JComboBox(new String[]{学号, 姓名, 班级}); JTextField keywordField new JTextField(12); JButton searchBtn new JButton(查询); queryPanel.add(fieldBox); queryPanel.add(keywordField); queryPanel.add(searchBtn); DefaultTableModel model new DefaultTableModel( new String[]{学号,姓名,性别,班级,体检日期,身高(cm),体重(kg),左眼视力}, 0); JTable table new JTable(model); table.setRowHeight(26); table.setFillsViewportHeight(true); JPanel buttonPanel new JPanel(); JButton addBtn new JButton(新增体检); JButton editBtn new JButton(修改); JButton delBtn new JButton(删除记录); JButton exportBtn new JButton(导出CSV); buttonPanel.add(addBtn); buttonPanel.add(editBtn); buttonPanel.add(delBtn); buttonPanel.add(exportBtn); frame.add(queryPanel, BorderLayout.NORTH); frame.add(new JScrollPane(table), BorderLayout.CENTER); frame.add(buttonPanel, BorderLayout.SOUTH); frame.setVisible(true);这段代码定义了整体骨架。setFillsViewportHeight(true) 让表格在窗口拉高时背景不再是一块灰白JScrollPane 必须包在表格外层否则表头不显示。这里刻意不用 GroupLayout 这类复杂布局桌面程序固定尺寸FlowLayout BorderLayout 更直观调试成本也低。4.2 把查询结果灌进表格ResultSet 转 TableModel 的正确姿势查询结果塞进 JTable 的关键是操作 DefaultTableModel而不是直接拿 ResultSet 给 JTable 用。最常见的问题是刷新时忘记清空数据导致每次查询都在旧行后面追加。正确顺序先 setRowCount(0) 清空再逐行 addRow。下面按“关键字模糊查询”演示三种字段SQL 用动态拼接而不是 WHERE ? 的形式public void query(String keyword) { model.setRowCount(0); StringBuilder sql new StringBuilder( SELECT s.student_id, s.full_name, s.gender, s.class_name, e.exam_date, e.height_cm, e.weight_kg, e.vision_left FROM student s LEFT JOIN physical_exam e ON s.student_id e.student_id WHERE 11 ); ListObject params new ArrayList(); if (keyword ! null !keyword.trim().isEmpty()) { sql.append( AND (s.full_name LIKE ? OR s.class_name LIKE ? OR s.student_id LIKE ?) ); String like % keyword.trim() %; params.add(like); params.add(like); params.add(like); } sql.append( ORDER BY s.class_name, e.exam_date DESC ); try (Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql.toString())) { for (int i 0; i params.size(); i) { ps.setString(i 1, (String) params.get(i)); } try (ResultSet rs ps.executeQuery()) { while (rs.next()) { model.addRow(new Object[]{ rs.getString(student_id), rs.getString(full_name), rs.getString(gender), rs.getString(class_name), rs.getDate(exam_date), rs.getBigDecimal(height_cm), rs.getBigDecimal(weight_kg), rs.getBigDecimal(vision_left) }); } } } catch (SQLException e) { JOptionPane.showMessageDialog(null, 查询失败 e.getMessage()); } }两个要点LEFT JOIN physical_exam 保证刚录入但还没做体检的学生也出现在列表里INNER JOIN 会把这类学生过滤掉ORDER BY 的 exam_date DESC 让最近一次体检排在最上面。rs.getDate() 得到 java.sql.Date放进表格模型后默认显示 yyyy-MM-dd不会出现带 T 的 ISO 时间戳。如果你用 rs.getObject() 图省事8.0 驱动可能返回 LocalDate表格显示就会出问题避坑章节会展开讲。4.3 新增和修改事务边界与字段校验放在哪一层新增一条体检记录界面弹一个 JDialog包含学号、体检日期、身高、体重、视力等输入框。保存时要做两层处理界面层负责格式校验DAO 层负责 SQL 执行。常见错误是把校验都堆在 DAO 里结果错误提示一闪而过用户不知道哪里填错。private void saveExamRecord() { String studentId idField.getText().trim(); String dateStr dateField.getText().trim(); String heightStr heightField.getText().trim(); if (studentId.isEmpty() || dateStr.isEmpty() || heightStr.isEmpty()) { JOptionPane.showMessageDialog(this, 学号、体检日期、身高必填); return; } LocalDate examDate; try { examDate LocalDate.parse(dateStr, DateTimeFormatter.ofPattern(yyyy-MM-dd)); } catch (DateTimeParseException e) { JOptionPane.showMessageDialog(this, 日期格式应为 yyyy-MM-dd); return; } if (!heightStr.matches(\\d{2,3}(\\.\\d)?)) { JOptionPane.showMessageDialog(this, 身高请输入两位或三位数可带一位小数); return; } String sql INSERT INTO physical_exam(student_id, exam_date, height_cm, weight_kg, vision_left) VALUES (?, ?, ?, ?, ?); try (Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, studentId); ps.setDate(2, java.sql.Date.valueOf(examDate)); ps.setBigDecimal(3, new BigDecimal(heightStr)); ps.setBigDecimal(4, weightField.getText().trim().isEmpty() ? BigDecimal.ZERO : new BigDecimal(weightField.getText().trim())); ps.setBigDecimal(5, visionField.getText().trim().isEmpty() ? BigDecimal.ZERO : new BigDecimal(visionField.getText().trim())); ps.executeUpdate(); JOptionPane.showMessageDialog(this, 保存成功); query(currentKeyword); } catch (SQLException e) { JOptionPane.showMessageDialog(this, 保存失败 e.getMessage()); } }这里示范了“校验在界面层、SQL 在 DAO 层”的常见做法。日期用 LocalDate.parse 而不是 SimpleDateFormat因为解析失败时前者抛 DateTimeParseException你能明确提示格式错误SimpleDateFormat 往往返回一个解析错误的值。身高用正则 \d{2,3}(\.\d)? 拦掉负数、字母和超长数字。体重、视力字段允许为空时用 BigDecimal.ZERO 兜底对应表里 NOT NULL DEFAULT 0 的语义。关于数据一致性体检记录和学生的备注更新如果发生在同一个点击事件里就应该放进同一个事务。先 setAutoCommit(false)全部执行成功再 commit任何一步失败 rollback。我在这类系统里只把“修改学生基本信息 清空过敏史备注”这种关联操作放事务普通单表插入不加事务减少无意义的锁等待。4.4 导出体检表CSV 是比 Excel 更稳的默认选项“能不能导出 Excel”是验收时必被问到的问题。Java 的 Apache POI 库确实能生成带图表的 xlsx但我的建议是先做 CSV 导出稳定且轻几行代码就能交付。POI 适合要做单元格合并、多 sheet、图表这种复杂报表的场景仅导出表格数据CSV 完全够用Excel 打开也正常。注意两点写 UTF-8 BOM否则 Excel 打开 CSV 中文乱码字段里有逗号时加双引号包裹。public void exportCsv(JTable table) { if (table.getRowCount() 0) { JOptionPane.showMessageDialog(null, 当前没有可导出的数据); return; } JFileChooser chooser new JFileChooser(); chooser.setSelectedFile(new File(体检档案导出_ System.currentTimeMillis() .csv)); if (chooser.showSaveDialog(null) ! JFileChooser.APPROVE_OPTION) { return; } File file chooser.getSelectedFile(); try (PrintWriter out new PrintWriter(file, UTF-8)) { out.print(\ufeff); TableModel m table.getModel(); for (int c 0; c m.getColumnCount(); c) { out.print(, m.getColumnName(c)); } out.println(); for (int r 0; r m.getRowCount(); r) { StringBuilder line new StringBuilder(); for (int c 0; c m.getColumnCount(); c) { Object v m.getValueAt(r, c); String cell v null ? : v.toString(); if (cell.contains(,) || cell.contains(\)) { cell cell.replace(\, \\); cell \ cell \; } line.append(,).append(cell); } out.println(line.substring(1)); } JOptionPane.showMessageDialog(null, 导出成功 file.getAbsolutePath()); } catch (IOException e) { JOptionPane.showMessageDialog(null, 导出失败 e.getMessage()); } }这里从 JTable 拿数据而不是再查一次数据库是为了保证“用户看到的和导出的一致”。表格可能已经做过关键字过滤再查一次反而对不上。字段含逗号时用双引号包裹是 CSV 规范不做这一步班级名“东校区,高一(3)班”这种数据会让 Excel 把一列拆成两列导出文件直接废掉。5. 常见问题排查连接失败、中文乱码和界面假死的定位顺序HealthArchives 这类 JDBC Swing 项目运行期报错就那几类。先把排查顺序记住驱动类有没有加载、MySQL 服务有没有启动、URL 参数对不对、字符集统不统一、数据库操作用没用在工作线程上。按这个顺序查绝大多数翻车都能在十分钟内定位。5.1 error 2002 与连接失败先检查服务、端口再看时区和 SSL 参数现象程序启动点查询按钮弹窗报 error 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock或者直接是 Communications link failure。原因error 2002 是 MySQL 客户端协议层的报错说明程序按 socket 文件路径去找 MySQL但那个路径上没有监听。常见于 MySQL 没启动、或者 URL 写了 localhost 让 JDBC 走了 socket。Communications link failure 则多半是端口 3306 不通、时区参数缺失、SSL 参数不对这三类之一。解决步骤第一确认 MySQL 服务已启动Windows 看服务管理器里 MySQL80Linux 用 systemctl status mysqld第二URL 里主机名统一写 127.0.0.1避免 localhost 触发 socket 连接第三URL 带上 serverTimezoneAsia/Shanghai 和 useSSLfalse。按这个顺序查九成连接问题都能消失。如果还不行就在 Java 里临时用 Socket 探一下端口new Socket(127.0.0.1, 3306)能通就继续查参数不能通则先解决 MySQL 启动问题。5.2 中文写入变成问号三层字符集缺一不可现象界面输入“张三”INSERT 进库变成“????”或者数据库里正常界面查出来乱码。原因字符集要三层一致MySQL 库表字符集、JDBC 连接串 characterEncoding、Java 源码文件编码。常见翻车组合是表用 utf8mb4连接串没写 characterEncodingJVM 默认编码又是 Windows 的 GBK中文到库就变问号。解决建库建表统一用 utf8mb4连接串显式加 characterEncodingutf8IDE 里把项目源码编码设成 UTF-8。三层一致之后中文问题基本不再出现。另外提醒一句不要轻易动 JVM 启动参数的 file.encodingSwing 组件字体渲染和它强相关改乱了界面会出现方块字比乱码更难修。用 Navicat 看到的问号也可能只是连接工具刷新慢了关掉查询窗口重开再看一次。5.3 界面点按钮后卡死Swing 事件线程不能直接跑数据库现象点“查询”窗口整个卡住拖动无响应几秒到十几秒后又恢复。单机小数据量时不明显数据量上万之后必现。原因Swing 是单线程模型所有界面更新都在 EDTEvent Dispatch Thread上执行。数据库查询是阻塞操作如果直接在按钮事件里执行 JDBCEDT 被占住界面就假死了。判断方法很简单在按钮事件第一行打印 Thread.currentThread().getName()如果输出是 AWT-EventQueue-0说明你确实跑在界面线程上。解决耗时查询放到 SwingWorker 后台线程查询结束后再切回 EDT 更新表格searchBtn.addActionListener(e - { String keyword keywordField.getText().trim(); new SwingWorkerVoid, Object() { Override protected Void doInBackground() { query(keyword); // 后台跑 JDBC不占 EDT return null; } Override protected void done() { statusLabel.setText(查询完成共 table.getRowCount() 行); } }.execute(); });这段代码的关键在 doInBackground 和 done 两个方法前者在子线程执行不阻塞界面后者回到 EDT可以安全更新 JLabel、JTable。不要把 model.addRow 放进 doInBackgroundJTable 数据模型不是线程安全的必须回到 EDT 再更新。如果懒得分至少把 PreparedStatement 和 ResultSet 的循环留在后台线程里界面更新只留一句 addRow 回填。5.4 日期和空值在表格里显示异常getObject 与渲染器边界现象某行身高空白表格里显示 0.0日期列显示成“2024-03-01T00:00:00”或者一串数字。原因身高体重是 DECIMAL 列用 rs.getBigDecimal 能正常拿小数点如果图省事用 rs.getObjectMySQL 8 驱动返回的可能是 BigDecimal 或 Double放进 Object[] 之后 JTable 按 toString 渲染空值的显示就五花八门。日期列用 rs.getObject 拿到 LocalDateJTable 默认渲染器不认 LocalDatetoString 输出就会带 T 字母。解决读日期用 rs.getDate()读数值用 getBigDecimal 或 getInt不要在 DAO 里统一 getObject。显示层面0.0 如果不想出现在表格里组装 model 行时判断一下 BigDecimal 等于零就写空串BigDecimal h rs.getBigDecimal(height_cm); model.addRow(new Object[]{ rs.getString(student_id), rs.getString(full_name), rs.getString(gender), rs.getString(class_name), rs.getDate(exam_date), h null || h.compareTo(BigDecimal.ZERO) 0 ? : h.toString() });这里用 compareTo 而不是 equals是因为 BigDecimal 的 equals 会连精度一起比较0.0 和 0.00 在 equals 眼里是两个值compareTo 才是数值大小比较。这条血泪经验适用所有财务、健康类数据的显示逻辑。6. 收尾技巧把 HealthArchives 打成可运行 jar 并顺手做数据备份验证一个系统做没做完我习惯跑一遍完整操作链登录、新增学生、录入体检、修改、删除、模糊查询、导出 CSV中间故意输入错误日期和空学号看界面有没有友好提示。这条链走完没弹异常窗口就可以交付了。交付前还有一个容易被忽略的点把程序打成可运行 jar双击而不是用 IDE 跑。IDE 里能跑不算能力换一台只装了 MySQL 的 Windows 机器java -jar 能起才算闭环。打包用 Maven 的话shade 插件把依赖的 mysql-connector-j 和 HikariCP 打成一个包。db.properties 放在 jar 外面而不是打进包内否则改数据库密码还得重新打包。jar 同目录放一个启动说明文本写清楚三件事MySQL 8 怎么安装、health_archives.sql 怎么导入、db.properties 从哪里改。这看起来是小事但答辩时换机器演示这份说明能省掉现场半小时的尴尬。数据备份这条是我的血泪经验。课程设计答辩前库里录了三百条演示数据一次 Navicat 误操作把表清空了差点重录一夜。后来的习惯是每天收工前跑一次 mysqldump命令就一行mysqldump -uroot -p health_archives health_archives_$(date %Y%m%d).sqlWindows 批处理里用 %date% 生成日期字符串把这行丢进计划任务每天自动备份一次。恢复时执行 mysql -uroot -p health_archives 备份文件.sql整个库连同索引约束一起回来比手工重新录入快太多。如果你的演示环境需要提前铺数据这份备份脚本本身就是最好的准备工具。最后一个建议来自我最初写类似系统翻车的教训别把 SQL 直接写在按钮事件里。新建一个 Dao 类集中管理所有 SQL界面层只调方法。这样后面加导出、加统计报表都是只改 DAO 层加方法界面代码一行不动。HealthArchives 这个规模不需要引入 MyBatis 级别的框架但简单的手写 DAO 分层足够让你在改需求和答辩问答时少改一半代码。这套习惯保持到现在项目越往后越轻松希望帮到你。本文还有配套的精品资源点击获取
返回列表