简介:这是一份面向数据库课程设计的人事管理系统完整项目,基于C#与Windows Forms构建,覆盖员工信息、考勤、薪资核算等核心人事业务,适合高校学生和入门开发者作为课程设计参考。资源共91个文件,压缩包约4.1MB,以cs源码、resx资源、docx文档为主,也包含mdf与ldf数据库文件、exe可执行程序和相关配置文件,整体可直接运行。内含数据库课程设计报告和系统运行说明书,前者说明需求分析、数据库模型与系统架构,后者提供安装与操作指引;源码覆盖登录、员工、部门、薪资、密码修改等多个功能模块,便于对照学习WinForms界面搭建、C#业务逻辑与SQL Server数据库的联动实现。已有超过5721人学习下载,适合想了解人事系统从数据表设计到功能落地的读者。
1. 为什么课程设计要选人事管理系统:从做界面转向做数据
人事管理系统在所有数据库课程设计里算是最能做扎实的选题之一,不是因为功能多,而是因为它的数据关系非常典型:部门、员工、考勤、工资、登录账号,五个实体一条业务线,正好覆盖建表、外键、增删改查和事务这些必考能力。很多同学一开始就把精力花在页面上,结果答辩时连“为什么这个字段要设唯一键”都答不顺,这恰恰是本末倒置。做这套课题的正确路线,是先花一半时间把数据模型想明白,再动手写代码。这篇文章就按这条路线走:从需求边界、ER 图、MySQL 建库建表,到登录和工资发放入账,最后把最容易翻车的几个坑一次性说清。
2. 先把“人事”理成关系模型:从需求边界到 ER 图的五个实体
2.1 需求边界:一份最小系统该有哪些表和字段
课程设计最忌讳“我要做所有功能”。人事管理系统如果真按企业人力资源系统来做,光权限和流程就可以写几十张表,一个学期都做不完。我一般建议把需求收拢到一条核心业务链:员工入职并分配到部门、每天打两次考勤、月底按考勤和基本工资结算薪资、员工用账号登录系统。
按这条链,最少需要五个实体。
| 实体 | 关键字段 | 职责 |
|---|---|---|
| 部门 dept | dept_id, dept_name, parent_id, manager_id | 维护部门层级 |
| 员工 employee | emp_id, emp_no, name, dept_id, hire_date, position, phone | 人员基本信息 |
| 考勤 attendance | att_id, emp_id, biz_date, clock_in, clock_out, status | 每天一条考勤流水 |
| 工资 salary | salary_id, emp_id, year_month, basic, bonus, deduction, net_salary | 按月生成工资流水 |
| 账号 sys_user | user_id, username, password_hash, role_code, emp_id | 登录与权限基础 |
这里有两个细节值得在设计文档里写清楚。第一,考勤表的 status 字段虽然可以由打卡时间推导出来,但把它存下来之后,月底统计“这个月迟到几次”就变成一条 count 加 group by 的 SQL,演示效果非常直观,这是典型的反向规范化。第二,工资表里 net_salary 可以用 basic + bonus - deduction 实时算出来,但我仍然建议把实发金额作为字段存起来,因为工资一旦发放就是历史快照,如果之后改了基本工资或扣款规则,历史记录不应该跟着变。
2.2 ER 图转关系模式:主外键怎么定
ER 图画完之后,要能讲清楚每一条关系是怎么落到表上的,否则答辩被问“你这外键为什么加在这边”就会卡壳。
| 关系 | 基数 | 实现方式 |
|---|---|---|
| 部门与员工 | 1 对 N | employee.dept_id 外键指向 dept.dept_id |
| 部门与上级部门 | 自关联 1 对 N | dept.parent_id 外键指向 dept.dept_id |
| 员工与考勤 | 1 对 N | attendance.emp_id 外键指向 employee.emp_id |
| 员工与工资 | 1 对 N | salary.emp_id 外键指向 employee.emp_id |
| 员工与账号 | 1 对 1 | sys_user.emp_id 外键加唯一约束 |
主键选择上,课程设计里最常见的错误是拿工号 emp_no 或者身份证号做主键。工号虽然唯一,但它属于业务字段,一旦公司调整编号规则会牵连所有外键;正确的做法是用自增的 emp_id 做代理主键,emp_no 单独加 unique 约束即可。emp_id 作为一个与业务无关的整数,唯一职责就是被其他表引用。至于员工和账号为什么是 1 对 1,是因为一个员工理论上只应该有一个登录账号,所以 sys_user.emp_id 上加 unique key,而不是普通外键。
2.3 范式权衡:该遵守到什么程度
课程设计的评分标准里,范式分析通常是加分项,但“强行满足范式”反而会把自己绕进去。教科书会告诉你第三范式要求不存在传递依赖,于是有些同学为了让工资表更“规范”,把基本工资拆到另一张工资标准表里,再通过岗位去关联。这样设计没有错,但演示时如果改一次奖金就要改两张表,出问题的概率大大增加。
我建议按第三范式约束主业务表,但允许两个明显的地方做冗余。第一个是 salary 表的 net_salary,它是可计算字段的冗余快照;第二个是 employee 表里的 dept_id 之外,不存 dept_name,只要 JOIN 就能查出部门名称,避免部门改名后员工表出现脏数据。做这种取舍时,答辩话术是“为了保留历史快照并减少联动修改”,这不是搪塞,而是生产系统里常见的确切需求。
3. 用 MySQL 落地:建库建表、索引与初始化数据的完整 DDL
3.1 建库与部门、员工表:引擎、字符集、注释一次到位
MySQL 是数据库课程设计里最常用的环境,版本建议选 8.0 以上,无论是 Docker 还是本地安装都行。建库时优先解决两件事:字符集用 utf8mb4 而不是 utf8,引擎用 InnoDB 而不是 MyISAM。
CREATE DATABASE IF NOT EXISTS hrms DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE hrms; CREATE TABLE dept ( dept_id INT AUTO_INCREMENT PRIMARY KEY COMMENT '部门id', dept_name VARCHAR(50) NOT NULL COMMENT '部门名称', parent_id INT NULL COMMENT '上级部门id,根节点为NULL', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_dept_name (dept_name), CONSTRAINT fk_dept_parent FOREIGN KEY (parent_id) REFERENCES dept(dept_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='部门表';这段 DDL 里有三个点值得跟评审老师讲清楚:自关联外键让同级部门表和上级部门挂钩,不设 name 字段时尽量避免 INNODB 对 TEXT 类型做前缀索引,后者会引发潜在性能瓶颈(即使这属于可能性)。CHARACTER SET utf8mb4 保证生僻字和 Emoji 不产生乱码,而 InnoDB 提供外键约束和事务支持,这两点 MyISAM 都做不到。
员工表紧接着建,它的 dept_id 指向刚刚生成的部门表。
CREATE TABLE employee ( emp_id INT AUTO_INCREMENT PRIMARY KEY COMMENT '员工id', emp_no VARCHAR(20) NOT NULL COMMENT '工号,业务编号', name VARCHAR(30) NOT NULL COMMENT '姓名', gender CHAR(1) DEFAULT '男' COMMENT '性别', birth_date DATE COMMENT '出生日期', hire_date DATE COMMENT '入职日期', dept_id INT NOT NULL COMMENT '所属部门', position VARCHAR(50) COMMENT '岗位', phone VARCHAR(20) COMMENT '联系电话', status TINYINT DEFAULT 1 COMMENT '1在职 0离职', UNIQUE KEY uk_emp_no (emp_no), CONSTRAINT fk_emp_dept FOREIGN KEY (dept_id) REFERENCES dept(dept_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工表';工号 emp_no 用 VARCHAR 而不是 INT,因为工号可能带前缀或补零,CHAR/VARCHAR 不会丢失前导零。status 字段是逻辑删除的开关,后面讲删除操作时会再展开。birth_date 和 hire_date 用 DATE 类型,不要在 Java 侧用字符串乱塞。
3.2 考勤、工资和登录账号表:流水表先想到“幂等”
考勤表是典型的高频写表,最容易犯的错误是没加唯一键,导致同一个员工同一天能插入多条记录。所以建表时要把 (emp_id, biz_date) 做成唯一键,从数据库层面挡住重复打卡。
CREATE TABLE attendance ( att_id INT AUTO_INCREMENT PRIMARY KEY, emp_id INT NOT NULL COMMENT '员工id', biz_date DATE NOT NULL COMMENT '业务日期', clock_in TIME NULL COMMENT '上班打卡时间', clock_out TIME NULL COMMENT '下班打卡时间', status ENUM('normal','late','early','leave') NOT NULL DEFAULT 'normal', remark VARCHAR(255) NULL COMMENT '备注,如请假原因', UNIQUE KEY uk_att_emp_date (emp_id, biz_date), CONSTRAINT fk_att_emp FOREIGN KEY (emp_id) REFERENCES employee(emp_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='考勤记录表';status 用了 ENUM,这是故意为之:它的可读性好,写 SQL 时一眼能看出值域。不过 ENUM 的扩展性差,如果后期要加“外勤”“出差”等状态就要改表结构,生产环境更推荐用 TINYINT 加字典表。课程设计阶段二选一都可以,但你要能说清楚为什么这样选,别稀里糊涂抄来就用。
工资表同样要防重复,唯一键放在 (emp_id, year_month) 上。
CREATE TABLE salary ( salary_id INT AUTO_INCREMENT PRIMARY KEY, emp_id INT NOT NULL COMMENT '员工id', year_month CHAR(6) NOT NULL COMMENT '月份,格式 202404', basic DECIMAL(10,2) NOT NULL COMMENT '基本工资', bonus DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '奖金', deduction DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '扣款', net_salary DECIMAL(10,2) NULL COMMENT '实发工资,冗余快照', status TINYINT NOT NULL DEFAULT 0 COMMENT '0草稿 1已发放', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_salary_emp_month (emp_id, year_month), CONSTRAINT fk_salary_emp FOREIGN KEY (emp_id) REFERENCES employee(emp_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工资流水表';DECIMAL(10,2) 是金额字段的标准选择,不要用 FLOAT 或 DOUBLE,它们有精度问题,算工资时尤其玄学。year_month 用 CHAR(6) 存储‘202404’,排序和比较都方便,MySQL 对定长字符串的检索也足够快。
登录账号表要单独建,不要把密码字段塞进 employee 表。
CREATE TABLE sys_user ( user_id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(30) NOT NULL COMMENT '登录名', password_hash VARCHAR(64) NOT NULL COMMENT '密码哈希,建议SHA-256或bcrypt', role_code VARCHAR(20) NOT NULL DEFAULT 'employee' COMMENT 'admin/hr/employee', emp_id INT NOT NULL COMMENT '关联员工', UNIQUE KEY uk_user_name (username), UNIQUE KEY uk_user_emp (emp_id), CONSTRAINT fk_user_emp FOREIGN KEY (emp_id) REFERENCES employee(emp_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='登录账号表';password_hash 这一列的长度按哈希算法来定,SHA-256 是 64 位十六进制,bcrypt 更长,我用 64 是给 SHA-256 留的位置。密码永远不要明文存储,这是数据库课设里少数几个“安全红线”,答辩必问。
还有一处容易忽略:dept 表里有个 manager_id 表示部门负责人,它和 employee 存在循环引用。正确做法是先建 dept 和 employee,然后用 ALTER 后补约束。
ALTER TABLE dept ADD CONSTRAINT fk_dept_manager FOREIGN KEY (manager_id) REFERENCES employee(emp_id);如果不这样做,建 dept 时 manager_id 引用 employee,而 employee 又要引用 dept,就会出现先有鸡还是先有蛋的问题。这段后补外键最好写进设计说明里,老师看到“先建表、后补外键”至少知道你考虑过依赖顺序。
3.3 初始化数据:让演示界面别一片空白
很多同学建完表直接开始写页面,等界面出来后才发现全是空表,查询没有任何观赏性。课程设计演示环节,两三张空表很难撑满五分钟,所以数据初始化要放在功能开发前做。
INSERT INTO dept (dept_name, parent_id) VALUES ('总公司', NULL), ('技术部', 1), ('人事部', 1); INSERT INTO employee (emp_no, name, gender, birth_date, hire_date, dept_id, position, phone, status) VALUES ('E001','张三','男','1998-05-12','2022-07-01',2,'Java工程师','13800000001',1), ('E002','李四','女','1996-09-08','2021-03-15',3,'人事专员','13800000002',1), ('E003','王五','男','1995-01-20','2020-11-01',1,'总经理','13800000003',1); UPDATE dept SET manager_id = 3 WHERE dept_id = 1; INSERT INTO attendance (emp_id, biz_date, clock_in, clock_out, status) VALUES (1, '2024-04-01', '08:55', '18:02', 'normal'), (1, '2024-04-02', '09:10', '18:30', 'late'), (2, '2024-04-01', '08:50', '17:40', 'normal'), (2, '2024-04-02', '08:45', '18:05', 'normal'), (3, '2024-04-01', '09:30', '12:00', 'leave');插入顺序必须遵循外键方向:先部门,再员工,再考勤,再工资。数据量不用太大,部门 3 个、员工 5 个、考勤 20 天左右就够,关键是让 SELECT、JOIN、GROUP BY 都有可展示的结果。如果嫌手写 INSERT 太慢,用 Navicat 的导入导出一口气生成也行,但要注意表顺序。
索引这块给个保守建议:不要为了“看起来厉害”到处加索引。employee.dept_id 上的外键约束会自动建索引,attendance 和 salary 已经各自有唯一键兼索引,典型查询都覆盖了。课程设计阶段索引不是主角,能说出“唯一键服务于防重,也承担查询加速”这句话就够用。
4. 业务功能实现:登录、增删改查与工资结算事务
4.1 登录校验怎么写:参数化 SQL 与权限隔离
登录是系统的门面,也是最能体现“你是否真的理解数据库操作”的地方。最稳妥的写法是参数化 SQL,也就是把用户输入的内容作为参数传给 PreparedStatement,而不是拼进字符串里。
SELECT u.user_id, u.role_code, u.emp_id, e.dept_id FROM sys_user u JOIN employee e ON u.emp_id = e.emp_id WHERE u.username = ? AND u.password_hash = ?;这里的两个问号由 JDBC 的 PreparedStatement 填充,MySQL 驱动会正确处理转义,杜绝 SQL 注入。答辩时如果老师问“为什么不直接拼字符串”,标准答案就是:字符串拼接会把单引号变成断点,' OR '1'='1这类输入可以直接绕过校验,这是数据库应用最容易犯的安全错误。
登录之后的权限控制不需要做多复杂,用 sys_user.role_code 一个字段就能区分三种角色。管理员看到“员工管理、薪资结算、部门管理”菜单,人事专员看到“考勤录入、信息修改”,普通员工只能查自己的考勤和工资。菜单显示归前端管,但后端 SQL 必须带过滤条件,否则改一下页面就能越权。
4.2 员工与部门的增删改查:外键约束、树查询与删除顺序
员工列表是最基本的查询,演示时建议直接用一条带 JOIN 的 SQL:
SELECT e.emp_no, e.name, d.dept_name, e.position, e.phone FROM employee e LEFT JOIN dept d ON e.dept_id = d.dept_id WHERE e.status = 1 ORDER BY e.emp_no;LEFT JOIN 和 INNER JOIN 在这个场景下结果一样,因为 employee.dept_id 不允许为空,但保留 LEFT JOIN 有个好处:以后扩展出“未分配部门”的待入职员工时,查询依然不丢数据。ORDER BY emp_no 让列表按工号排序,界面看着整齐。
新增员工时要注意外键连续校验,先确认 dept_id 指向的部门存在,再执行 INSERT。
INSERT INTO employee (emp_no, name, dept_id, hire_date, position, phone) VALUES (?, ?, ?, ?, ?, ?);删除是课设里藏雷最多的地方。如果直接DELETE FROM employee WHERE emp_id=1,而 attendance 表里有这个人的考勤记录,MySQL 会报外键约束失败。我推荐的做法是把“离职”实现为逻辑删除:
UPDATE employee SET status = 0 WHERE emp_id = ?;这样考勤和工资历史都保留,列表查询里也不会再出现离职员工。如果题目明确要求物理删除,那就要按“子表到父表”的顺序删:先 DELETE 考勤,再 DELETE 工资,再 DELETE 账号,然后 DELETE 员工,最后才能删部门。这个顺序要写进数据库设计报告里,是外键知识的直接体现。
部门表还有个树结构查询问题,MySQL 8.0 可以用递归 CTE 一次性把部门层级查出来:
WITH RECURSIVE dept_tree AS ( SELECT dept_id, dept_name, parent_id, 1 AS depth FROM dept WHERE parent_id IS NULL UNION ALL SELECT d.dept_id, d.dept_name, d.parent_id, dt.depth + 1 FROM dept d JOIN dept_tree dt ON d.parent_id = dt.dept_id ) SELECT depth, dept_id, dept_name FROM dept_tree ORDER BY depth, dept_id;递归 CTE 的原理是:先查出根部门,再反复 JOIN 出下一层,直到没有新记录为止。depth 表示层级,页面渲染时可以用来做缩进。如果你的课设环境是 MySQL 5.7,这个语法用不了,就得用 Java 循环多次查询或者存储过程实现,这也可以作为版本对比写在报告里。
4.3 考勤录入与工资结算事务:用 JDBC 把多条 SQL 放进同一个事务
考勤补卡和迟到修正,最理想的 SQL 是“没有记录就插入,有记录就更新”。
INSERT INTO attendance (emp_id, biz_date, clock_in, clock_out, status) VALUES (?, CURDATE(), ?, NULL, 'late') ON DUPLICATE KEY UPDATE clock_out = VALUES(clock_out), status = VALUES(status);ON DUPLICATE KEY UPDATE是 MySQL 对幂等写入的常用姿势,配合之前建表时加的 uk_att_emp_date 唯一键,同一天重复打卡不会产生第二条记录。这里的 VALUES() 写法在 MySQL 8.0.20 之后被标记为废弃,但课程设计常用,实际生产环境可以用新别名语法,这里我不展开,避免你答辩被追问时答混。
工资结算是全系统对事务要求最高的功能,因为“查询当月考勤、计算扣款、写入工资流水、更新发放状态”必须在一个事务里完成,任何一个环节失败都不能留下半截数据。
public void settleOneEmployee(Connection conn, int empId, String yearMonth, BigDecimal baseSalary) throws SQLException { String countLateSql = """ SELECT COUNT(*) AS late_cnt FROM attendance WHERE emp_id = ? AND DATE_FORMAT(biz_date, '%Y%m') = ? AND status = 'late' """; String insertSql = """ INSERT INTO salary (emp_id, year_month, basic, bonus, deduction, net_salary, status) VALUES (?, ?, ?, 0, ?, ?, 0) """; int lateCnt; try (PreparedStatement ps = conn.prepareStatement(countLateSql)) { ps.setInt(1, empId); ps.setString(2, yearMonth); try (ResultSet rs = ps.executeQuery()) { rs.next(); lateCnt = rs.getInt(1); } } BigDecimal deduction = BigDecimal.valueOf(lateCnt * 50L); BigDecimal net = baseSalary.subtract(deduction); try (PreparedStatement ps = conn.prepareStatement(insertSql)) { ps.setInt(1, empId); ps.setString(2, yearMonth); ps.setBigDecimal(3, baseSalary); ps.setBigDecimal(4, deduction); ps.setBigDecimal(5, net); ps.executeUpdate(); } }这段的调用入口要包事务:
public void doSettle(String yearMonth, BigDecimal baseSalary) throws SQLException { try (Connection conn = dataSource.getConnection()) { conn.setAutoCommit(false); conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); settleOneEmployee(conn, 1, yearMonth, baseSalary); settleOneEmployee(conn, 2, yearMonth, baseSalary); conn.commit(); } }逻辑说明:setAutoCommit(false) 关闭自动提交,之后所有 SQL 都在同一事务里;setTransactionIsolation 设为 READ_COMMITTED,比 MySQL 默认的 REPEATABLE READ 锁更少,适合工资这种读多写少的场景。try-with-resources 保证 Connection、PreparedStatement、ResultSet 都自动关闭,这在连接池环境下极其重要,一个连接泄漏就能让系统跑十几分钟就卡死。
这里的 salary 表有 (emp_id, year_month) 唯一键,所以即使程序被重复调用,也不会插入两条 4 月工资,最多报主键冲突,这就是幂等设计。第 5 章会专门说这个问题。
5. 课程设计避坑实录:乱码、外键、重复数据和连接池的 5 个现场
5.1 中文乱码:Navicat 正常,程序里全是问号
现象:在 Navicat 里执行 INSERT 中文一切正常,页面上查出来却是???或者浣犲ソ这种乱码。
原因:数据库字符集和客户端连接字符集不一致。你建库用了 utf8mb4,但 JDBC 连接串没指定字符编码,或者 JSP/页面本身的编码是 GBK,数据从 MySQL 到应用再到浏览器,某一环就转了码。
解决:先统一数据库端,建库建表都用 utf8mb4;再统一连接串,MySQL 8 的 JDBC URL 加上?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai;最后检查开发工具的源码文件编码,保证存盘时不是 GBK。这三层全对齐,乱码基本绝迹。
5.2 外键导致“删不掉”:数据库在保护不让你删
现象:想删除一个离职员工或空部门,MySQL 直接报错:Cannot delete or update a parent row: a foreign key constraint fails。
原因:这就是外键约束在起作用。你要删的员工在 attendance 或 salary 里有子记录,子表的 emp_id 指向了它,数据库不允许你制造“考勤属于一个不存在的人”这种脏数据。
解决:按子表到父表的顺序删,或者干脆用软删除,把 employee.status 改成 0。课程设计答辩时,老师问起这个报错,你可以直接回答“这是外键约束的保护机制,不是 bug”,顺便把数据完整性讲一遍,反而能加分。
5.3 同一员工同一天有多条考勤,统计翻倍
现象:月底统计出勤天数,数据比实际工作日多出一大截,比如张三 4 月只上班 20 天,却查出 35 条正常出勤。
原因:考勤表没有唯一键,程序里打卡接口被连点两次,或者批量导入脚本跑了两遍,同一 (emp_id, biz_date) 就插入了多条记录。
解决:给 attendance 加唯一键ALTER TABLE attendance ADD UNIQUE KEY uk_att_emp_date (emp_id, biz_date);,写操作换成INSERT ... ON DUPLICATE KEY UPDATE。如果想把补签和迟到分开,可以再加一个来源字段,但防重的唯一键必须有。
5.4 工资重算轻轻松松翻倍
现象:结算 4 月工资后发现有数据不对,改了一下扣款规则重新结算,结果每个员工都生成了两条 4 月工资,总额直接翻倍。
原因:salary 表没有对 (emp_id, year_month) 做唯一约束,程序里也没检查“这个月是不是已经结算过”,于是第二次执行继续 INSERT 新记录。
解决:建表时加上唯一键 uk_salary_emp_month。事务里可以先 SELECT 判断是否已存在,但更稳的是让唯一键兜底——重复执行时靠报错或ON DUPLICATE KEY UPDATE挡住。工资功能宁可“操作一次”,也别设计成“可以重复点”,这才是真正的后悔药。
5.5 演示十几分钟后连接超时
现象:功能刚开始正常,过了十几分钟按钮开始转圈,后台报Connection is not available, request timed out。
原因:最常见的是代码里拿到 Connection 后没用完关闭。Java 连接池的可用连接是有限的,如果每次操作都conn = dataSource.getConnection()却忘了 close,连接不会真实断开,而是被池子占着,很快就会耗尽。
解决:所有数据库操作都放在 try-with-resources 里,让 Connection、Statement、ResultSet 自动关闭;如果用的不是 try-with-resources,那也要在 finally 里逐个 close。写完功能后专门跑一次压力测试,连点页面一百次,连接池连接数不回涨,基本就没有泄漏问题。
6. 让答辩加分的三个数据库技巧:视图、存储过程与触发器
6.1 用一个视图把考勤统计变成“一张表”
月考勤统计是必查功能,与其让页面端拼 SQL,不如在数据库端建一个视图,把统计逻辑收拢到一处。视图对调用方来说就是一张只读表。
CREATE VIEW v_month_attendance AS SELECT DATE_FORMAT(biz_date,'%Y%m') AS year_month, e.emp_no, e.name, COUNT(*) AS work_days, SUM(a.status = 'late') AS late_days FROM attendance a JOIN employee e ON a.emp_id = e.emp_id GROUP BY year_month, e.emp_id, e.emp_no, e.name;视图创建后,页面查询直接SELECT * FROM v_month_attendance WHERE year_month='202404'就行。注意 GROUP BY 里要带 e.emp_id,因为 emp_no 虽然是唯一键,但要避免 ONLY_FULL_GROUP_BY 的坑,把员工主键一起分组最稳妥。缺点是对 year_month 的筛选会走全表扫描,但课设数据量小,不用优化。
6.2 把工资结算做成存储过程
工资结算逻辑放在 Java 里看得见摸得着,但放一个存储过程在数据库端,答辩演示可以更直接:调用一次,salary 表多几条记录,评委能当场看到效果。
DELIMITER $$ CREATE PROCEDURE sp_settle_salary(IN p_month CHAR(6), IN p_basic DECIMAL(10,2)) BEGIN INSERT INTO salary (emp_id, year_month, basic, bonus, deduction, net_salary, status) SELECT a.emp_id, p_month, p_basic, 0, SUM(a.status = 'late') * 50.00 AS deduction, p_basic - SUM(a.status = 'late') * 50.00, 0 FROM attendance a WHERE DATE_FORMAT(a.biz_date, '%Y%m') = p_month GROUP BY a.emp_id ON DUPLICATE KEY UPDATE deduction = VALUES(deduction), net_salary = VALUES(net_salary); END$$ DELIMITER ;调用方式是CALL sp_settle_salary('202404', 8000.00);。存储过程适用的原因是它把“查考勤、算扣款、写工资”三步封装成一个原子调用,省去 Java 层多次往返。注意DATE_FORMAT(biz_date, '%Y%m')在数据量大时不走索引,课设无所谓,但报告里最好写一句“生产环境应该用范围条件biz_date BETWEEN '2024-04-01' AND '2024-04-30'来优化”,显得你考虑过索引命中的问题。VALUES() 语法在 MySQL 8.0.20 以后被标记废弃,如果你的 MySQL 版本太新,换成AS new别名写法。
6.3 触发器:给员工表变动留一条审计记录
触发器属于很容易做过头的东西,但不加又显得技术点单薄。我建议只做一个:员工所属部门变动时,自动把 Old 值和 New 值写进审计表。
CREATE TABLE emp_audit ( audit_id INT AUTO_INCREMENT PRIMARY KEY, emp_id INT NOT NULL COMMENT '员工id', old_dept_id INT NULL, new_dept_id INT NULL, changed_at DATETIME DEFAULT CURRENT_TIMESTAMP, operator VARCHAR(30) NULL ); DELIMITER $$ CREATE TRIGGER trg_emp_dept_update AFTER UPDATE ON employee FOR EACH ROW BEGIN INSERT INTO emp_audit(emp_id, old_dept_id, new_dept_id, operator) VALUES (NEW.emp_id, OLD.dept_id, NEW.dept_id, CURRENT_USER()); END$$ DELIMITER ;这段触发器的作用是:只要 employee 表的 dept_id 被 UPDATE,就自动插入一条审计记录。演示时可以当着老师的面改一个员工的部门,然后查询 emp_audit 表,看到旧部门、新部门和操作时间,这个“数据留痕”的说服力比空讲强很多。需要提醒的是,触发器会增加写操作的隐形成本,而且排错困难,所以“一个触发器只做一件事”,别把工资算法也塞进去。
演示前最后一个小习惯:把每个单选按钮、删除按钮都点一遍,尤其是会触发外键报错的地方,提前想好怎么解释。我个人的做法是把“人工制造一次外键约束失败”当成开场演示,先让数据库报错,再现场讲清楚为什么删除被阻止,反而比一路顺畅更让人信服。这个习惯帮我挡过不少次突然的翻车。这套方案不需要你追求炫技,把数据关系写稳、把事务边界守住,它就是一个能完整讲透数据库课程的成品。希望帮到你。
本文还有配套的精品资源,点击获取