基于Java的工资管理系统毕业设计:从零实现到避坑指南
1. 背景痛点:为什么“能跑”≠“能毕业”?
第一次做毕业设计,大多数同学都会把“能跑起来”当成终点。可真正到了答辩现场,老师一句“如果员工并发调薪,你的事务能保证一致性吗?”就让场面瞬间凝固。下面这几个坑,90% 的 Java 项目都会踩:
- 紧耦合:所有 SQL 直接写在 JSP 里,Service 层纯当“传话筒”,后期换个数据库差点把键盘掀了。
- SQL 注入:用 Statement 拼接字符串,演示时把
or 1=1当工号输入,全表数据秒出,老师微笑不语。 - 事务失效:Spring 声明式事务一个
@Transactional就完事,结果方法被同类内部调用,回滚直接罢工。 - 权限裸奔:把“是否为管理员”存 Cookie,前端改 0 为 1 直接进后台,演示当场社死。
- 代码复制:每个模块都有一段“分页+模糊查询”的重复代码,导师问“你这叫 DRY?”只能尴尬陪笑。
痛定思痛,下面给出一条“新手也能复制”的逃生路线。
2. 技术选型:Spring Boot 为什么赢?
| 维度 | Servlet + JSP | Spring Boot + MyBatis |
|---|---|---|
| 依赖管理 | 手动导 jar,版本冲突到怀疑人生 | 起步依赖,Maven 一行搞定 |
| 配置量 | web.xml、spring.xml 双份暴击 | 约定大于配置,yml 即可 |
| 监控 | 自己写 filter 统计 QPS | actuator 端点直接看 |
| 部署 | 打 war 丢 tomcat,本地和线上环境不一致 | 内嵌容器,jar 包一键java -jar |
JPA vs MyBatis 怎么选?毕业设计场景里,复杂报表、灵活字段是常态。JPA 的自动 SQL 在“多表+动态条件”下常常翻车,而 MyBatis 写 SQL 直观,调优空间更大,对新手调试友好。一句话:能看懂 SQL,才能睡得踏实。
3. 模块化设计:先画饼再写码
3.1 工程结构(Clean Code 版)
salary-system ├─ common // 工具、常量、全局异常 ├─ employee // 员工档案 ├─ salary // 薪资计算核心 ├─ security // 登录、鉴权、密码加密 ├─ report // 报表导出 └─ admin // 系统管理(字典、参数)每个模块都按controller → service → mapper → entity/dto/vo四层展开,同层只依赖下层,禁止横向调用,编译期就能发现循环依赖。
3.2 数据库 ER 简图
- 员工表 t_employee(id, name, dept_id, base_salary ...)
- 部门表 t_department
- 用户表 t_user(登录用)
- 角色表 t_role & 中间表 t_user_role
- 工资表 t_salary(月份+员工联合唯一索引,保证幂等)
3.3 关键接口设计
- 员工调薪:PUT
/employee/{id}/salary传入“新基本工资”,记录旧值→写流水→更新主表,三步一个事务。 - 工资计算:POST
/salary/calculate?month=2024-05传入月份,批量异步计算,接口只返回任务编号,避免超时。 - 权限注解:自定义
@RequiresRoles("HR"),利用 Spring AOP 拦截,比写 if-else 清爽太多。
4. 核心代码:薪资计算 Service 片段
下面给出“月薪=基本工资+绩效-社保-个税”的最小可运行示例,已填注释,可直接粘贴进 IDE 跑单测。
@Service @Slf4j @RequiredArgsConstructor public class SalaryCalculationService { private final EmployeeMapper employeeMapper; private final SalaryItemMapper itemMapper; private final SalaryMapper salaryMapper; /** * 计算指定月份工资,保证幂等:若已存在则跳过 */ @Transactional(rollbackFor = Exception.class) public void calculateMonthSalary(String month) { // 1. 查询所有在职员工 List<Employee> employees = employeeMapper.selectActive(); for (Employee e : employees) { // 2. 幂等校验 if (salaryMapper.exists(e.getId(), month)) { log.warn("salary already exists, skip empId={}", e.getId()); continue; } // 3. 组装各项 BigDecimal base = e.getBaseSalary(); BigDecimal perf = itemMapper.sumPerformance(e.getId(), month); BigDecimal social = SocialUtil.calc(base); // 社保固定比例 BigDecimal tax = TaxUtil.calc(base.add(perf).subtract(social)); // 累计预扣 BigDecimal finalAmount = base.add(perf).subtract(social).subtract(tax); // 4. 落库 Salary s = Salary.builder() .empId(e.getId()) .month(month) .baseSalary(base) .performance(perf) .social(social) .tax(tax) .finalSalary(finalAmount) .build(); salaryMapper.insertSelective(s); } } }单元测试怎么写?用@SpringBootTest + @Transactional自动回滚,既能断言,又不污染库,毕业答辩现场演示屡试不爽。
5. 安全与性能:把“能跑”升级成“能扛”
5.1 密码加密
- 明文存密码 = 0 分;MD5 哈希 = 50 分;BCrypt 加盐 = 100 分。
- Spring Security 自带
BCryptPasswordEncoder,encode 后长度 60 位,验证时无需自己盐值。
5.2 SQL 注入
- MyBatis
#{}占位符预编译,SQL 注入概率趋近于 0。 - 动态排序字段不能用
#{},用<choose>+白名单校验,防止 order by 后面被拼接。
5.3 并发与事务
- 批量更新工资时,在 t_salary 建唯一索引
(emp_id, month),利用数据库抛DuplicateKeyException实现幂等。 - 事务方法一定要 public,同类自调用请用 AopContext 或拆到新 Bean,否则 Spring 无法代理。
5.4 简单压测
- 本地笔记本 4C8G,连接池默认 10 线程,2000 员工 * 12 月历史回算,平均 RT 1.3s,CPU 峰值 60%,无 FullGC。
- 把 Druid 连接池调到 maxActive=30,RT 降到 0.7s,足够毕设答辩“性能”章节交差。
6. 生产环境避坑指南
数据库连接池
阿里云 MySQL 5.7 默认 max_connections=200,毕设演示时把连接池 maxActive 设 100,结果老师同时开 5 台手机热点刷新,直接把库打挂。调回 30 并加等待超时,世界瞬间安静。事务回滚失效
捕获异常后 log 一把,又 throw 出去,事务正常回滚;若 catch 后吞掉异常,Spring 以为你“已处理”,不回滚,数据当场裂开。前端 Mock
本地没网?用 Mock.js 拦截 Ajax,返回随机工资条,演示流畅不掉链。记得上线前把 mock 开关干掉,否则真发钱时发现数字全是假的,HR 要来砍人。日志级别
生产环境 root 级别 INFO 足够,把 SQL 日志调成 DEBUG 会刷屏,磁盘 1 天 3G,云盘报警比工资条先到。端口与防火墙
云服务器 3306 只对 Web 机开放,后台管理端口 8080 走 Nginx 反向代理,再配 HTTPS,答辩老师问“安全怎么做”时,直接把锁形图标甩给他看。
7. 留给你的思考题
单公司场景跑通后,只要再加一个company_id字段,所有表按租户隔离,登录时把公司编码塞进 JWT,就能升级成 SaaS 多租户。或者把“导出工资 Excel”功能排进日程:用阿里巴巴 EasyExcel,2 行代码把 2 万条数据流式写出,内存稳在 50M 以内,老师看了都说“像商业软件”。
毕业设计不是句号,而是把“写能跑的代码”练成“写能用的系统”。下一步,你会把多租户、分布式锁、微服务都安排上吗?评论区聊聊你的计划,我们下篇再见。