☰
企业工资管理系统Java课程设计:从ER模型到JDBC三层架构
2026/10/6 8:37:03 网站建设 项目流程

简介:面向数据库课程设计的《企业工资管理系统(Java版+完整代码)》是一份完整的课程设计报告,内容覆盖需求分析、总体设计、详细设计、系统实现与实验总结,适合高校学生在完成数据库原理及应用课程设计时参考仿写。报告从员工信息、基本工资、津贴三类核心数据出发,明确了基本工资与津贴设定、月工资计算、工资信息录入/添加/修改等主要功能,并给出性能需求(默认精度3位小数、响应时间不超过0.5秒、支持跨平台数据互通)、数据流图、E-R图以及功能模块划分。详细设计部分列出了员工表、基本工资表、津贴信息表的字段结构与主键约束,针对职工信息管理、工资管理、职工登录查询三个模块分别说明;系统实现部分则配有界面截图和Java设计代码,完整呈现了基于MySQL数据库的工资管理系统的落地过程。资源包内共1个doc文档,大小约261KB,内容精炼集中。目前已有766人学习,对需要快速理解Java+MySQL数据库应用开发流程、完成课程设计报告的同学有较高参考价值。

1. 数据库课程设计选企业工资管理系统,到底值在哪

很多人选课设题目时有个误区:觉得名字越新潮越好,电商秒杀、推荐系统、AI识别一股脑往上堆,结果数据库设计撑不起来,答辩时被老师问几句就露馅。企业工资管理系统看起来朴素,却是把数据库增删改查、事务、多表查询、完整性约束这些考点全部串起来的标准场景,几乎每个学期的优秀课设里都有它的身影。标题里“java版+完整代码”意味着你不用从零造轮子,重点是把成型的工程读懂、改成自己的,再配合一篇能说清设计思路的课程设计说明书。这门课设适合计算机专业三、四年级学生,也适合想补一块完整项目经验的初级开发者。把三张表和一套 JDBC 代码吃透,比堆一堆炫技框架稳得多。

2. 需求分析与表结构设计:三张表怎么撑起一门课设

工资管理系统听起来功能很多,拆开看就是三件事:管部门、管员工、管每个月发工资。很多人的问题不是表建得不够多,而是关系理不清。最典型的反面案例是把所有字段塞进一张大表,员工所属部门名在每一行里重复存储,改一次部门名要 UPDATE 几十行,这种设计在答辩时基本属于送人头。用第三范式把数据拆开,再靠外键把关系重新连接起来,才是课程设计该有的样子。

2.1 为什么是“员工-部门-工资”三张表:ER模型与范式

先画 ER 图再做表,这是顺序问题。ER 图里三个实体分别是部门、员工、工资记录,关系也很清楚:部门对员工是 1:N,员工对工资记录是 1:N。翻译成表结构就是:

  • dept 表:只存部门自身的信息,部门ID、部门名称、负责人;
  • employee 表:存员工固定属性,工号、姓名、所属部门、岗位、基本工资、入职日期;
  • salary_record 表:存每个月给每个员工发放的工资明细,属于“每月发生一次”的事实数据。

这里要特别说清楚为什么工资单独建表而不是加在员工表里。员工表里如果放了“本月工资”字段,下个月发工资时只能覆盖上个月的数据,历史记录全丢了。工资是时间维度的数据,它的特点是按月产生一条,必须独立成表,通过 emp_id 关联员工。这个思路叫作“把固定信息和流水信息分开存储”,说明书里写明白这一点,老师会觉得你真的理解设计目标,而不是在堆表。

范式层面,这套设计满足第三范式:每个非主键字段都直接依赖主键,不产生传递依赖。员工表通过 dept_id 关联部门,部门名称没有冗余到员工表;工资表只存 emp_id,员工姓名也要通过关联去查。实际课设里不需要刻意强调“我用了第几范式”,但被问到时你要能指出,明明是有意识做的设计,不是歪打正着。

2.2 建库建表SQL:字段类型、默认值与外键约束

建库脚本是整套代码的地基。我用 MySQL 8.X 写了一份可以直接跑的 DDL,字符集用 utf8mb4,避免测试数据里出现生僻字时插入报错。

CREATE DATABASE salary_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE salary_db; CREATE TABLE dept ( dept_id TINYINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '部门ID', dept_name VARCHAR(50) NOT NULL UNIQUE COMMENT '部门名称', manager VARCHAR(20) DEFAULT NULL COMMENT '部门负责人' ) ENGINE=InnoDB COMMENT='部门表'; CREATE TABLE employee ( emp_id VARCHAR(10) PRIMARY KEY COMMENT '工号,例如 E001', emp_name VARCHAR(50) NOT NULL COMMENT '姓名', dept_id TINYINT UNSIGNED NOT NULL COMMENT '所属部门ID,关联dept表', position VARCHAR(50) NOT NULL DEFAULT '普通员工' COMMENT '岗位名称', base_salary DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '基本工资', hire_date DATE NOT NULL COMMENT '入职日期', status TINYINT NOT NULL DEFAULT 1 COMMENT '1在职 0离职', CONSTRAINT fk_emp_dept FOREIGN KEY (dept_id) REFERENCES dept(dept_id) ) ENGINE=InnoDB COMMENT='员工表'; CREATE TABLE salary_record ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, emp_id VARCHAR(10) NOT NULL COMMENT '员工工号', salary_month CHAR(7) NOT NULL COMMENT '工资月份,格式2025-01', base_salary DECIMAL(10,2) NOT NULL COMMENT '基本工资', position_salary DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '岗位工资', bonus DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '奖金', deduction DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '扣款合计', real_salary DECIMAL(10,2) NOT NULL COMMENT '实发工资', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '发放时间', CONSTRAINT fk_salary_emp FOREIGN KEY (emp_id) REFERENCES employee(emp_id), UNIQUE KEY uk_emp_month (emp_id, salary_month) ) ENGINE=InnoDB COMMENT='工资发放记录表';

这份表结构里每个选择都有理由,答辩被问到时不能只说“别人都这么建”。salary_month 用 CHAR(7) 而不是 DATE,因为工资月份只有年和月,没有日,DATE 类型反而会引入“2025-01-00”这种非法日期问题,且 CHAR(7) 定长存储,配合 LIKE '2025-%' 做月份范围查询效率更高。金额字段全部用 DECIMAL(10,2),10 位有效数字,其中 2 位小数,这正好能支撑千万级累计金额,别用 FLOAT,二进制浮点数存钱会出精度问题,后面避坑章专门讲。

工资表上的 UNIQUE KEY uk_emp_month 是业务规则的体现:一个员工同一个月只能有一条工资记录。这个唯一索引比在 Java 代码里先查后插可靠得多,因为它是在数据库层面兜底,哪怕两个人同时点击“发放工资”,数据库也只会允许一条插入成功。外键默认是 RESTRICT 策略,删除有工资记录的员工时会直接报错,这是保护历史数据的安全机制,先让员工离职(status 置 0)而不是物理删除。

2.3 索引与约束:三个值得写进说明书的设计细节

建表只是第一步,说明书里如果能写出下面三个细节,档次立刻不一样。

第一个细节是外键的删除策略。ON DELETE CASCADE 看着方便,删部门连带删员工,但演示时一旦误删,整批数据全没了,这种设计在真实系统里属于高危操作。课设推荐保留默认的 RESTRICT,并且在说明书里写明:离职员工的信息要在业务层面保留,不物理删除,工资历史才能追溯。一句话解释“我为什么不用级联删除”,老师就知道你考虑过数据安全。

第二个细节是唯一索引承担了类似锁的防重职责。多线程场景下,真实系统发工资要考虑数据库并发锁,防止同一员工被重复发放;课设不需要真的去演示锁冲突,但可以在数据库层用唯一索引等效实现这个约束。在说明书的“创新点”里写“通过数据库唯一索引代替应用层判重,减少一次查询”,比空谈并发更实在。

第三个细节是 salary_month 字段要建普通索引。工资系统最常见的查询是“查某个月所有人的工资”和“查某个人所有月份的工资”,前者会反复用到 salary_month 作为 WHERE 条件。没有索引时全表扫描,数据量到几万条后明显卡顿;加了索引后查询走 B+ 树,速度是数量级提升。索引不是越多越好,每个索引都会拖慢插入速度,工资表只在 emp_id 和 salary_month 上建立必要索引就够了。

注意:如果你要在老版本 MySQL 上跑,建库语句里的 utf8mb4 需要 MySQL 5.5 以上才支持,5.1 之前只能用 utf8。现在课程设计基本都用 8.X,这个问题遇到再处理。

3. 用 JDBC + 三层架构落地 Java 源码:从 DBUtil 到 SalaryDAO

表结构定好之后,Java 代码怎么组织是第二个关键决策。我见过不少同学用 Swing 写了一坨上千行的类,界面逻辑和 SQL 混在一起,跑是能跑,答辩时根本讲不清。课程设计不是软件工程课设,但也至少要表现出基本的“分层意识”。这里的常见做法是 JDBC + 三层架构:实体类对应表,DAO 层只做增删改查,Service 层写业务规则,UI 层负责和用户交互。

3.1 三层结构怎么切:entity、dao、service、ui 的类规划

为什么不推荐在这个课设里直接上 Spring Boot?因为数据库课程设计的评分点恰恰是 SQL 和 JDBC 基本功,Spring Boot 把连接管理、事务、参数绑定全部封装掉了,代码里看不到 PreparedStatement,也讲不出连接关闭的时机。用原生 JDBC 写一遍,你对数据库操作的理解深度完全不一样。框架可以放在“改进方向”里提一句,而不是作为主线。

常见的包结构是这样的:

包名职责典型类
entity与三张表一一对应的实体类,字段与表字段同名Employee、Dept、SalaryRecord
dao数据访问,只写 SQL 和 JDBC 代码,不写业务判断EmployeeDao、SalaryDao
service业务逻辑,组合 DAO 完成工资计算、发放、汇总SalaryService
ui控制台菜单或 Swing 窗体,只做输入输出MainFrame、LoginDialog

这个分层的核心思想是“改动一处不连累其他处”。比如数据库从 MySQL 换成 Oracle,只需要改 DBUtil 和 DAO 里的 SQL,界面完全不动;比如工资计算公式变了,只改 Service 层,DAO 不需要知道。答辩时你可以直接指着一行代码说:这里是 DAO,它只负责和数据库打交道;这里是 Service,业务规则都沉淀在这一层。这就是面向对象编程 java 落地时最朴素也最好用的组织方式。

3.2 DBUtil 连接工具类:db.properties 配置与连接池参数

不管哪个 DAO 都要拿数据库连接,这个动作必须抽出来。用配置文件而不是把连接串硬编码在代码里,换数据库环境时不需要重新编译,这也是课设的加分项。

package util; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.Properties; public class DBUtil { private static String driver; private static String url; private static String user; private static String password; static { try (InputStream in = DBUtil.class.getResourceAsStream("/db.properties")) { Properties props = new Properties(); props.load(in); driver = props.getProperty("jdbc.driver"); url = props.getProperty("jdbc.url"); user = props.getProperty("jdbc.user"); password = props.getProperty("jdbc.password"); Class.forName(driver); } catch (Exception e) { throw new ExceptionInInitializerError("数据库配置初始化失败,请检查 db.properties"); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } }

配套的 db.properties 放在 src 根目录下:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/salary_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false jdbc.user=root jdbc.password=123456

说明几个关键参数。jdbc.url 里 useUnicode 和 characterEncoding 是中文不乱码的前提,编码必须和建库时的 utf8mb4 一致。serverTimezone 是 MySQL 8.X 必须加的参数,不加会报时区错误。useSSL=false 是本地开发关闭 SSL 加密,避免证书告警。password 是数据库密码,演示前要确认和本机一致。Class.forName 的作用是触发驱动类的静态初始化,把驱动注册到 DriverManager,MySQL 8.X 的驱动类名是 com.mysql.cj.jdbc.Driver,老写法 com.mysql.jdbc.Driver 在新驱动里已经不存在了。

这个工具类能直接跑通,但它是最简单的 DriverManager 直连模式,每次 getConnection 都会新建物理连接。课设阶段并发量低,这么做没问题;如果你在说明书里写“后续可以引入连接池”,建议用 HikariCP,并在连接池配置里把 maximumPoolSize 控制在 10 以内,因为 MySQL 默认最大连接数是 151,连接池开太大反而把自己搞挂。

3.3 DAO 层写法:PreparedStatement 完成增删改查,避免 SQL 注入

DAO 层最常见的错误是拿字符串拼 SQL。比如"SELECT * FROM employee WHERE emp_id = '" + id + "'",这种写法一旦用户输入E001' OR '1'='1,查询条件就变成恒真,整张表被拖出来。JDBC 提供 PreparedStatement 解决的就是这个问题,参数用问号占位,由驱动完成转义和预编译。

package dao; import entity.Employee; import util.DBUtil; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class EmployeeDao { public boolean insertEmployee(Employee e) { String sql = "INSERT INTO employee (emp_id, emp_name, dept_id, position, base_salary, hire_date) " + "VALUES (?,?,?,?,?,?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, e.getEmpId()); ps.setString(2, e.getEmpName()); ps.setInt(3, e.getDeptId()); ps.setString(4, e.getPosition()); ps.setBigDecimal(5, e.getBaseSalary()); ps.setDate(6, new java.sql.Date(e.getHireDate().getTime())); return ps.executeUpdate() == 1; } catch (SQLException ex) { ex.printStackTrace(); return false; } } public Employee findById(String empId) { String sql = "SELECT emp_id, emp_name, dept_id, position, base_salary, hire_date " + "FROM employee WHERE emp_id = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, empId); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { Employee e = new Employee(); e.setEmpId(rs.getString("emp_id")); e.setEmpName(rs.getString("emp_name")); e.setDeptId(rs.getInt("dept_id")); e.setPosition(rs.getString("position")); e.setBaseSalary(rs.getBigDecimal("base_salary")); e.setHireDate(rs.getDate("hire_date")); return e; } } } catch (SQLException ex) { ex.printStackTrace(); } return null; } }

这里 try-with-resources 是 Java 7 以后的写法,Connection、PreparedStatement、ResultSet 都实现了 AutoCloseable,代码块结束后自动关闭,顺序是先关 ResultSet,再关 Statement,最后关 Connection。很多人课设里的“连接泄漏”就是忘了在 finally 里关闭资源,用这个写法可以从机制上避免。

要注意实体类里的 BigDecimal 字段和数据库的 DECIMAL 对应,用 rs.getBigDecimal 读取,不要图省事先 getString 再转,容易出现精度损失。如果工资表用自增主键,插入后要拿到新 ID,通常在 prepareStatement 时加一个参数Statement.RETURN_GENERATED_KEYS,再用 getGeneratedKeys() 获取,这在员工表用自增 ID 的场景下会用到。

4. 工资计算、部门汇总与防重复发放:业务逻辑的正确打开方式

表结构和 DAO 准备齐全后,工资系统的“灵魂”在 Service 层。这里包括工资怎么算、部门怎么汇总、发放时怎么保证不重复、不半途失败。很多课设代码能跑通,但业务规则散落在界面事件里,改一个公式要找半天,这节就讲清楚怎么把业务逻辑做扎实。

4.1 工资计算公式放 service 层:怎么算实发、怎么校验

工资的计算公式可以定成:实发工资 = 基本工资 + 岗位工资 + 奖金 - 扣款合计。扣款合计里包含请假扣款、迟到扣款、五险一金个人部分。计算本身不复杂,复杂的是三个绑定的原则:用 BigDecimal 计算、校验结果合法性、不把公式写进 DAO 或 UI。

package service; import entity.SalaryRecord; import java.math.BigDecimal; public class SalaryService { public BigDecimal calcRealSalary(SalaryRecord r) { BigDecimal real = r.getBaseSalary() .add(r.getPositionSalary()) .add(r.getBonus()) .subtract(r.getDeduction()); if (real.compareTo(BigDecimal.ZERO) < 0) { throw new IllegalArgumentException("实发工资不能为负数,请检查扣款数据"); } if (real.compareTo(new BigDecimal("1000000.00")) > 0) { throw new IllegalArgumentException("实发工资超过百万上限,疑似数据录入错误"); } return real.setScale(2, BigDecimal.ROUND_HALF_UP); } }

这里使用 BigDecimal 而不是 double,原因是二进制浮点数没法精确表示 0.1,多次累加后会出现 0.30000000000000004 这种结果,工资差几分钱对不上账,在答辩演示时非常尴尬。setScale(2, ROUND_HALF_UP) 表示保留两位小数,四舍五入。校验分两端:实发为负直接抛异常,这是最常见的脏数据来源,比如录入扣款时把三位数当成两位数;超过百万的校验是我个人习惯,防止测试时手滑多敲两个零。业务校验放在 Service 层而不是 DAO 层,因为 DAO 的职责是存取数据,不知道“工资不能为负”这条业务规则。

4.2 按部门汇总与按月查询:一条 GROUP BY 让答辩有话说

课程设计说明书里如果只有增删改查,评语往往是“功能完整但缺乏亮点”。加一个“按部门查询月度工资汇总”功能,SQL 的含金量立刻不一样,因为它涉及多表连接、分组聚合和结果过滤三个考点。

SELECT d.dept_name, COUNT(s.id) AS emp_count, SUM(s.base_salary) AS total_base, SUM(s.position_salary) AS total_position, SUM(s.bonus) AS total_bonus, SUM(s.deduction) AS total_deduction, SUM(s.real_salary) AS total_real FROM salary_record s JOIN employee e ON s.emp_id = e.emp_id JOIN dept d ON e.dept_id = d.dept_id WHERE s.salary_month = '2025-01' GROUP BY d.dept_id, d.dept_name HAVING SUM(s.real_salary) > 0 ORDER BY total_real DESC;

这条 SQL 里有三个答辩高频考点。第一是 JOIN 的写法,工资表关联员工表拿到 dept_id,再关联部门表拿到部门名称,而不是在工资表里冗余存一个部门名字段。第二是 GROUP BY 与 SELECT 的约束:select 后面的非聚合列(dept_name)必须出现在 GROUP BY 里,MySQL 在默认 sql_mode 下对这项检查比较宽松,但换到严格模式会直接报错,这也是很多人在老环境能跑、新环境报错的原因。第三是 WHERE 和 HAVING 的分工:WHERE 在分组之前过滤行,HAVING 在分组之后过滤组。这里“工资月份等于某月”属于行级过滤,放 WHERE;如果要求过滤掉总金额小于某个值的部门,就得用 HAVING。两者不能互换。

在 Java 里执行这条查询时,结果集的处理方式和普通查询不同,每组返回一行,需要封装成一个汇总实体或 Map。我一般会建一个 DeptSalarySummary 类,字段和查询列一一对应,然后循环 ResultSet 填充。

4.3 防重复发放:事务 + 唯一索引双保险

发工资不是单条插入,而是“给所有在职员工一次性生成当月记录”。真实场景下最怕的情况是:老板点击“发放 2025-01 工资”,程序跑了 10 条,第 11 条因为某种原因失败,前 10 条却留在数据库里。数据库事务专门解决这类问题:要么全部提交,要么全部回滚。

public void paySalaryForMonth(String salaryMonth) { String sql = "INSERT INTO salary_record " + "(emp_id, salary_month, base_salary, position_salary, bonus, deduction, real_salary) " + "VALUES (?,?,?,?,?,?,?)"; try (Connection conn = DBUtil.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps = conn.prepareStatement(sql)) { for (Employee e : employeeDao.findAllActive()) { SalaryRecord r = buildRecord(e, salaryMonth); ps.setString(1, e.getEmpId()); ps.setString(2, salaryMonth); ps.setBigDecimal(3, r.getBaseSalary()); ps.setBigDecimal(4, r.getPositionSalary()); ps.setBigDecimal(5, r.getBonus()); ps.setBigDecimal(6, r.getDeduction()); ps.setBigDecimal(7, r.getRealSalary()); ps.addBatch(); } ps.executeBatch(); conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } } catch (SQLException e) { e.printStackTrace(); } }

setAutoCommit(false) 关闭自动提交,之后所有 SQL 都在同一个事务里执行;中间任何一步抛异常,rollback 会把整个批次撤销,不会留下“发了一半工资”的脏数据。executeBatch() 把多条 insert 打包发给 MySQL,减少网络往返,数据量大时性能提升明显。

事务保证的是原子性,唯一索引保证的是幂等性。假设程序没做检查,同一个月连续点了两次“发放”,第一次成功插入 100 条,第二次执行到第一条就撞上唯一索引报错,整个事务回滚,不会出现重复数据。如果只靠 Java 代码里先SELECT COUNT(*)再决定是否插入,在并发场景下会存在检查与插入之间的空窗期,两次请求可能同时通过检查,最后插入两条重复记录。数据库层的唯一索引是最后一道防线,这也呼应了数据库并发锁那一类问题在工程上的常见解法:能由数据库约束保证的,不要完全依赖应用层判断。

5. 避坑指南:连不上库、中文乱码、精度丢失等高频翻车点

课设翻车的高峰期不是写代码的时候,而是答辩前一晚部署到老师电脑上的时候。以下五个问题我在辅导课设时见得太频繁,每一条都是“现象 → 原因 → 解决”的完整链路,提前踩一遍,答辩时就不慌。

5.1 驱动加载失败:ClassNotFoundException 与 MySQL 时区报错

现象:程序启动报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver,或者能加载驱动但连接时报The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized。

原因:MySQL 8.X 之后驱动类名改成了com.mysql.cj.jdbc.Driver,老名字只在 5.X 驱动里存在。而时区报错是 MySQL 8.X 的新要求,连接串里必须显式声明 serverTimezone。

解决:驱动类名改成com.mysql.cj.jdbc.Driver,url 后面拼上serverTimezone=Asia/Shanghai。如果老师机器上装的是 MySQL 5.7,com.mysql.jdbc.Driver还能用,但建议统一按 8.X 改,向下兼容没问题。这个报错最常见的原因是资料包里的 db.properties 是几年前写的,拿到后第一件事就是检查这两处。

5.2 中文乱码:控制台问号和数据库里的乱码

现象:从控制台插入中文,查询出来是“?”,或者直接报Incorrect string value: '\xE5\xBC\xA0...'。

原因:字符集不统一。数据库是 utf8mb4,连接串没指定 characterEncoding,客户端控制台是 GBK,三处只要有一处不一致就会乱码。

解决:三处对齐。建库时指定DEFAULT CHARACTER SET utf8mb4(这个在前面建表语句里已经做了);连接串加useUnicode=true&characterEncoding=utf8mb4;IDE 控制台编码改成 UTF-8,IDEA 在 Settings → Editor → File Encodings 里把 Global Encoding 和 Project Encoding 都改成 UTF-8。还有一个隐蔽坑:db.properties 文件本身如果用 Windows 记事本编辑另存为 GBK,里面的中文注释也会坏,建议用 IDE 编辑 properties 文件。

5.3 连接没关:运行一会报 Too many connections

现象:程序跑了几次操作后,突然报Too many connections,重启项目又好了,过一会儿又犯。

原因:DAO 里获取了 Connection 但没关闭。DriverManager 直连模式下,每次 getConnection 都会建立新的物理连接,MySQL 默认最大连接数 151,连接不释放很快耗尽。还有一种情况是引入了连接池但 maximumPoolSize 设得过大,比如 HikariCP 默认 10,但如果同时在跑多个项目实例,也会顶爆上限。

解决:所有 JDBC 操作改成 try-with-resources,确保 Connection、Statement、ResultSet 在异常时也能关闭。排查时先执行SHOW PROCESSLIST;看有没有大量 Sleep 状态的连接,有就说明代码里有连接没关。如果课设里确实想用连接池,把 maximumPoolSize 调到 5 到 10,并让连接池管理连接的生命周期,而不是每次都新建。

5.4 工资算错:浮点数精度翻车

现象:实发工资计算结果显示 12345.670000000002 这种尾巴,或者几条工资加起来总和和计算器对不上,差几分钱。

原因:Java 的 double 和数据库的 FLOAT 都基于 IEEE 754 二进制浮点数,0.1 在二进制里是无限循环小数,存储时被截断,运算误差累积后就暴露了。工资这种金额数据根本不能用浮点类型承载。

解决:数据库金额字段全部用 DECIMAL(10,2),Java 端实体类用 java.math.BigDecimal,DAO 读取用 getBigDecimal,计算用 add/subtract/multiply,禁止用 double 转 BigDecimal,尤其不要用new BigDecimal(0.1),要用BigDecimal.valueOf(0.1)或字符串构造。这个坑属于“平时不炸,一演示就炸”的类型,答辩前最好用一组带小数的测试数据手工验一遍。

5.5 外键约束拦路:删除部门报错删不掉

现象:删除部门表某条记录时,报错Cannot delete or update a parent row: a foreign key constraint fails。

原因:employee 表还有员工引用这个 dept_id,外键默认的 RESTRICT 策略不允许删除被引用的父记录。这是保护机制在工作,不是代码 bug。

解决:分情况处理。如果要删的部门里没有员工,直接删;如果有,要先把这个部门的员工转到其他部门(UPDATE employee SET dept_id = ?),或者把员工标记为离职再处理。千万不要在演示时硬删,也不建议为了省事把外键去了,外键是数据库完整性约束的一部分,课程设计的评分标准里明确有它。说明书里应写清楚“员工离职采用逻辑删除,历史工资记录保留可查”,这比物理删除更能体现工程意识。

6. 答辩是最后一道坎:三个加分改造与演示自检清单

课程设计做到能跑只是及格,答辩时让老师觉得“有设计、有思考”才是拿高分的关键。这里分享三个投入产出比最高的改造,都是在现有 JDBC 三层架构上做加法,不伤筋动骨。

第一个是导出工资表到 Excel。利用 Apache POI 把按月份查询的结果集写成 .xls 文件,代码量不大,但在演示时非常直观。老师可能只问一句“能不能导出”,这个功能一展示,整条链路就从“数据库查询”延伸到“文件输出”,也呼应了企业系统里常见的“工资表要发给财务”的真实需求。如果时间紧张,只做导出不做导入,答辩时再提一句“导入可以用同样的思路解析 Excel 后批量入库”,想法到位就够了。

第二个是加操作日志表。这是很多人想不到但老师很看重的点。建一张login_log或者op_log表,记录“谁在什么时间做了什么操作”,登录成功后插入一条,敏感操作(发放工资、删除员工)也插入一条。它本身很简单,但体现了审计意识,答辩时可以说“系统需要追溯操作记录,所以设计了日志表”。

第三个是演示前做一次全流程自检,也是我认为最重要的一个习惯。我当年答辩吃过亏:功能全是好的,但演示机上的 MySQL 服务没启动,启动后密码不对,最后只能用截图硬撑。自检清单其实就三行:MySQL 服务是否运行,db.properties 的账号密码是否对,测试数据是否完整。这三件事花五分钟就能确认,却决定了你能不能顺利开场。老师不一定在乎功能多炫,但一定在乎你的系统是不是一个“随时能跑”的状态。

课设是很奇妙的一件事,代码量不一定要大,但每一个设计点都要能解释清楚。真正拉开差距的往往不是技术复杂度,而是你有没有认真对待那些“显而易见”的细节:字段类型选对了吗,资源关了吗,事务加了吗,外键在保护什么。希望这些经验能帮你少踩几个坑,把这次课程设计做成简历里拿得出手的一笔。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询