1. 选题背后的门道:为什么薪资管理系统值得做
计算机毕业设计最怕什么?不是不会写代码,而是选了一个自己都讲不清楚的题目。薪资管理系统这个方向我接触过不少,从本科生到在职提升的都有人选,最后能做出彩的,往往不是技术堆得最猛的,而是把业务逻辑吃得最透的。Spring Boot 在这个题目里几乎成了标准答案:生态成熟、案例多、招人喜欢,更重要的是,薪资这个场景天然自带“计算规则 + 权限控制 + 数据报表”三大模块,每一块都能撑起论文的核心章节。
很多人上来就纠结“我这个题目是不是太简单了”。我直接说结论:薪资管理系统一点都不简单,但要看你做到什么程度。如果只是做一个增删改查,把员工表和工资表绑在一起,那确实撑不起一篇像样的毕业设计论文。可如果你把工资计算规则、社保公积金扣除、个税累计预扣、工资条发放、权限分级、报表导出这些环节都理清楚,这个题目的深度完全不输给那些花里胡哨的“基于某某框架的某某平台”。
选这个题还有一个隐性的好处:薪资结构每个学校、每个公司都不一样,这意味着你可以在论文里名正言顺地写“本系统针对XX场景设计”,不用怕查重,也不用怕答辩时被问住。答辩老师最常问的一句话就是“你这个工资是怎么算出来的”,只要业务逻辑是你自己梳理的,这个问题你就能答得比背代码流畅得多。
从评分角度看,毕业设计一般看三样东西:工作量、创新点、系统完成度。薪资管理系统在这三方面都有天然的着力点。工作量方面,员工管理、部门管理、薪资项目配置、月考勤汇总、工资计算、历史工资查询、统计图表,随便一列就是六七个模块。创新点方面,你可以做电子工资条的加密查看,也可以做薪资变动的审批流,还可以做基于历史数据的薪资趋势预测。完成度方面,Spring Boot 加 Vue 的前后端分离方案已经很成熟,照着规范的流程走,很少会翻车。
2. 技术选型的权衡:Spring Boot 不是唯一答案,但一定是最稳的答案
2.1 后端框架:为什么选 Spring Boot 而不是 SSM 或者微服务
这个题目如果你去搜索引擎里翻,十个里有八个是 Spring Boot 做的。原因很简单:Spring Boot 把配置的复杂度降下来了,让你把精力放在业务实现上。SSM 时代你要写一堆 XML 配置,光 Spring、Spring MVC、MyBatis 三者的整合就能折磨掉你两周时间,而这些工作对毕业设计来说没有任何加分。Spring Boot 的自动装配机制把这些约定俗成的配置都处理掉了,你只需要关注自己写的代码。
我见过一些同学上来就想搞 Spring Cloud 微服务、分布式事务、消息队列,觉得这样显得水平高。说实话,毕业设计阶段搞微服务,大概率是给自己挖坑。薪资管理系统本质上是企业内部系统,并发量不会高到哪里去,单体应用完全够用。微服务带来的服务拆分、配置中心、链路追踪这些问题,每一个都能让你在答辩前夜崩溃。老老实实用 Spring Boot 单体架构,把代码写得干净利落,比什么都强。
版本选择上我多说一句,不要一上来就装最新版。Spring Boot 3.x 对 JDK 版本有要求,而且部分第三方组件的兼容性不如 2.x 稳定。我做这个项目的时候用的是 Spring Boot 2.7.x + JDK 1.8,这套组合经过了大量的生产验证,网上能搜到的坑和解决方案也最多。如果你非要用新版本,也要保证自己的 JDK 版本跟得上,别装了个 Spring Boot 3.2 还在用 JDK 8,启动都会报错。
2.2 前端方案:前后端分离还是服务端渲染
薪资管理系统这种项目,前端有两种典型做法。第一种是传统的服务端渲染,用 Thymeleaf 模板引擎,后端把数据填到 HTML 里返回给浏览器。第二种是前后端分离,后端只提供 JSON 接口,前端用 Vue 或者 React 单独开发,最后打包成静态文件部署。
我给的建议是:如果你前端基础一般,选前后端分离反而更容易通过答辩。这个逻辑听起来反直觉,但实际情况是,前后端分离是目前企业开发的主流形态,论文里可以写的技术点更多。你可以光明正大地写“前端采用 Vue 3 + Element Plus,后端采用 Spring Boot,通过 RESTful API 进行数据交互”。这句话在技术描述里非常加分。而且 Vue 的学习曲线没那么陡,照着 Element Plus 的文档做表单和表格,几天就能上手。
前端打包之后的部署也别慌,Vue 项目执行npm run build后会在dist目录下生成静态文件。有两种部署方式:一种是把dist文件夹放到 Nginx 下,配置反向代理到后端接口;另一种是把静态文件直接放进 Spring Boot 项目的src/main/resources/static目录下,打成 Jar 包后一个进程跑起来,浏览器直接访问 8080 端口就能看到页面。第二种方式对毕设演示更友好,不用额外装 Nginx,打个包就能在答辩现场的电脑上跑起来。
2.3 持久层框架:MyBatis-Plus 是省时间的神器
持久层你肯定会纠结:用 MyBatis 还是 MyBatis-Plus?我的态度很明确:做毕设就用 MyBatis-Plus。它把单表的 CRUD 操作全都封装好了,你不需要写那些重复的 insert、update、selectById,只需要定义一个继承了BaseMapper<T>的接口,就能直接调用这些方法。更重要的是它的条件构造器LambdaQueryWrapper,查询员工列表、按部门过滤、按薪资区间筛选,写起来非常直观,也容易在论文里展示。
有个细节要注意:MyBatis-Plus 的分页插件需要单独配置一个PaginationInnerInterceptor,不配置的话Page对象虽然能创建出来,但实际查出来的数据是全表,分页不生效。这个地方我当年踩过坑,在答辩演示的时候发现“下一页”按钮点了没有反应,排查了半天才发现是拦截器没有注册。
2.4 项目结构:一个能让你在答辩时讲得清楚的包划分
项目结构的好坏直接决定你答辩时讲代码的流畅程度。我建议采用标准的controller/service/mapper/entity四层结构,再加一个config包放配置类,一个common包放通用返回结果和异常处理。具体如下:
com.example.salary ├── controller # 接口层,接收前端请求 ├── service # 业务层,处理核心逻辑 ├── mapper # 数据访问层,MyBatis-Plus的Mapper接口 ├── entity # 实体类,对应数据库表 ├── dto # 数据传输对象,接收前端参数 ├── vo # 视图对象,返回给前端的数据 ├── config # 配置类,如拦截器、CORS配置 ├── common # 公共类,如统一返回结果、异常处理 └── utils # 工具类,如Excel导出工具这里重点说一下dto和vo的区别。很多同学分不清这两个东西,其实很简单:dto是前端传给你的参数对象,比如新增员工时传过来的EmployeeAddDTO;vo是你返回给前端的数据对象,比如工资条详情SalaryDetailVO。把这两个分开,好处是接口的参数和返回值不会跟着实体类走,你改数据库表结构的时候不会把前端接口也带崩。答辩的时候老师问“为什么需要 DTO”,你能说出“为了避免实体类直接暴露给前端,同时也是为了参数校验统一”这样的话,印象分会高不少。
3. 业务核心拆解:薪资计算的来龙去脉
3.1 需求梳理:从零开始画出系统的完整模块
薪资管理系统看着简单,真正做起来模块可不少。我按照自己实现过的方案,把核心模块梳理了一遍:
| 模块名称 | 核心功能 | 难易程度 |
|---|---|---|
| 员工管理 | 员工信息的增删改查、部门调动、入职离职状态管理 | 简单 |
| 部门管理 | 部门树形结构、部门负责人设置 | 简单 |
| 薪资项目管理 | 配置基本工资、岗位工资、绩效奖金、补贴等薪资项 | 中等 |
| 考勤汇总 | 从考勤数据计算出勤天数、请假天数、加班时长 | 中等 |
| 薪资计算 | 根据薪资项目和考勤数据,结合社保公积金规则计算应发工资 | 难 |
| 工资条管理 | 生成工资条、员工查看自己的工资明细、工资条导出 | 中等 |
| 用户管理 | 登录用户、角色分配、菜单权限控制 | 中等 |
| 统计报表 | 部门薪资汇总、月度薪资趋势、人员成本分析 | 中等 |
需求梳理阶段最容易犯的错误是:把所有的功能一把抓,什么都想做,结果什么都没做精。这里我给一个非常实用的建议:先画出核心流程,再围绕流程补外围功能。核心流程就是“员工入职 → 每月考勤 → 月底薪资计算 → 工资条发布 → 员工查看确认”。围绕着这个流程,你会发现真正必须的只有员工管理、考勤汇总、薪资计算、工资条这四个环节,其他的都是锦上添花。
3.2 数据库设计的核心思路:宁可多拆表,不要全塞一起
数据库设计是我最想强调的部分。很多人的表结构设计出来,一个employee表恨不得放下所有人的信息,一个salary表既有应发工资又有实发工资还有个税社保,这样的设计后期一定返工。数据库设计的核心原则就是:高内聚,低耦合,一个表只做一件事。
对于薪资管理系统,我推荐至少设计这几张表:
-- 员工表 CREATE TABLE employee ( id BIGINT PRIMARY KEY AUTO_INCREMENT, emp_no VARCHAR(20) NOT NULL UNIQUE COMMENT '工号', name VARCHAR(50) NOT NULL COMMENT '姓名', dept_id BIGINT COMMENT '部门ID', position VARCHAR(50) COMMENT '岗位', phone VARCHAR(20), email VARCHAR(50), hire_date DATE COMMENT '入职日期', status TINYINT DEFAULT 1 COMMENT '1在职 0离职', create_time DATETIME, update_time DATETIME );-- 薪资项目表 CREATE TABLE salary_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, item_name VARCHAR(50) NOT NULL COMMENT '项目名称', item_type TINYINT COMMENT '1固定项 2计算项 3扣除项', formula VARCHAR(200) COMMENT '计算公式描述', sort_order INT COMMENT '排序', is_active TINYINT DEFAULT 1 );-- 员工薪资项目关联表 CREATE TABLE employee_salary_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, emp_id BIGINT NOT NULL, item_id BIGINT NOT NULL, amount DECIMAL(10,2) DEFAULT 0 COMMENT '固定金额', effective_date DATE COMMENT '生效日期' );这里有一个很重要的设计思想:不要把薪资直接写成员工表的一个字段,而是用“薪资项目”的方式来配置。为什么?因为工资结构是会变的——这个月多了个交通补贴,下个月绩效调整了,如果直接在员工表上加减字段,改表结构会非常痛苦。用“员工-薪资项目”关联表,你就可以动态地给不同员工配置不同的薪资项,数据库不需要动,业务上还灵活。
3.3 工资计算规则:把业务逻辑讲清楚比写代码更重要
工资计算是整个系统最核心也最容易让答辩老师追问的地方。很多同学的工资表就是“基本工资 + 绩效 - 请假扣款”,这样太简单了,经不起推敲。我建议你把计算规则设计得稍微细一点,哪怕实现的时候简化一些,但设计上要有层次感。
我当时的设计是这样:每个月工资 = 应发工资 - 社保个人部分 - 公积金个人部分 - 个人所得税 + 补贴。应发工资 = 基本工资 + 岗位工资 + 绩效工资 + 加班费。这些项目中,基本工资和岗位工资是从员工薪资配置里取的,绩效工资是绩效分数乘以绩效基数,加班费是加班时长乘以小时工资率。请假扣款 = 请假天数 * 日工资,日工资 = 基本工资 / 当月应出勤天数。
个税部分如果不想做得太复杂,可以用最新的累计预扣法,但核心计算过程要讲清楚。哪怕你的系统只实现了“按月度税率表计算当月个税”,在论文里也要写明采用的是哪种方式,为什么这样设计。答辩老师不会要求你把全套税务算法都做出来,但你要让他知道,你理解这个业务,而不是只会调库。
3.4 角色权限与数据隔离:越简单越要防漏洞
薪资数据属于敏感数据,权限控制做不好,答辩的时候会被追问得体无完肤。最基础的要求是:
- 管理员:可以查看所有员工的薪资,可以配置薪资项目,可以执行薪资计算和发放
- 部门经理:只能查看本部门员工的薪资
- 普通员工:只能查看自己的工资条
这种三级权限模型不需要引入 Spring Security 这种重量级框架,你可以用一个简单的拦截器来实现。在HandlerInterceptor里检查当前登录用户的角色,然后结合 URL 的规则来决定是否放行。比如/api/salary/detail/**只允许本人访问,/api/salary/list需要部门经理以上权限,/api/salary/calculate只能是管理员。
拦截器实现有一个很容易忽略的细节:数据级别的权限过滤一定不能只在前端做。比如部门经理查看本部门员工薪资,前端界面上你可以只显示本部门的数据,但后端接口也必须做校验——前端传来的deptId参数不能直接信,要拿当前登录用户的部门 ID 去比对。否则别人模拟请求一下,就能看到其他部门的薪资数据。
4. 核心模块实现:从配置到代码的关键细节
4.1 项目初始化的关键依赖
创建 Spring Boot 项目时,pom.xml里这几个依赖是少不了的。我的版本组合是 Spring Boot 2.7.8 + MyBatis-Plus 3.5.3 + Hutool 5.8.x + EasyExcel 3.x,没有用最新的版本,就是为了稳定。
<dependencies> <!-- Web 相关 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis-Plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- Hutool 工具类(加密、日期处理等) --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.22</version> </dependency> <!-- EasyExcel 导入导出 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>3.3.2</version> </dependency> <!-- Lombok 简化代码 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>这里要特别说一句,hutool-all真的很适合毕设项目。它的SecureUtil.md5()可以快速做密码加密,DateUtil可以方便地处理日期加减,ExcelUtil虽然不如 EasyExcel 灵活,但做简单的导入模板生成也很顺手。这种工具类能帮你省掉大量重复代码,在论文里写“使用 Hutool 工具类提高开发效率”也是一句话亮点。
4.2 工资计算 Service 的代码实现
工资计算的核心逻辑我建议放在一个独立的SalaryCalculateService里。这个方法要经历这么几步:查询员工的薪资项目配置、查询当月的考勤汇总、计算各种补贴和扣款、把结果组装成工资明细插入工资表。
用一个简化版的代码来讲解:
@Service public class SalaryCalculateService { @Autowired private EmployeeSalaryItemMapper employeeSalaryItemMapper; @Autowired private AttendanceSummaryMapper attendanceSummaryMapper; @Transactional public CalculateResult calculate(int year, int month) { // 1. 查询所有在职员工 List<Employee> employees = employeeMapper.selectList( new LambdaQueryWrapper<Employee>() .eq(Employee::getStatus, 1) ); for (Employee employee : employees) { // 2. 查询员工薪资配置项 List<EmployeeSalaryItem> items = employeeSalaryItemMapper.selectList( new LambdaQueryWrapper<EmployeeSalaryItem>() .eq(EmployeeSalaryItem::getEmpId, employee.getId()) .eq(EmployeeSalaryItem::getEffectiveDate, year + "-" + month + "-01") ); // 3. 查询考勤汇总 AttendanceSummary attendance = attendanceSummaryMapper.selectOne( new LambdaQueryWrapper<AttendanceSummary>() .eq(AttendanceSummary::getEmpId, employee.getId()) .eq(AttendanceSummary::getMonth, year + "-" + month) ); // 4. 遍历薪资项目,累加计算 BigDecimal baseSalary = BigDecimal.ZERO; BigDecimal bonus = BigDecimal.ZERO; BigDecimal deduction = BigDecimal.ZERO; for (EmployeeSalaryItem item : items) { switch (item.getItemType()) { case 1: baseSalary = baseSalary.add(item.getAmount()); break; case 2: bonus = bonus.add(item.getAmount()); break; case 3: deduction = deduction.add(item.getAmount()); break; } } // 5. 扣考勤缺勤 if (attendance != null && attendance.getAbsentDays() > 0) { BigDecimal daySalary = baseSalary.divide( BigDecimal.valueOf(attendance.getShouldWorkDays()), 2, RoundingMode.HALF_UP ); deduction = deduction.add( daySalary.multiply(BigDecimal.valueOf(attendance.getAbsentDays())) ); } // 6. 汇总结果 SalaryDetail detail = new SalaryDetail(); detail.setEmpId(employee.getId()); detail.setMonth(year + "-" + month); detail.setBaseSalary(baseSalary); detail.setBonus(bonus); detail.setDeduction(deduction); detail.setNetSalary(baseSalary.add(bonus).subtract(deduction)); // 保存明细 } return result; } }有几个细节值得讲一下。第一,BigDecimal一定要用String或者int构造,不要用double构造,不然会出现金额精度问题。第二,除法运算必须指定精度和舍入模式,divide(BigDecimal.valueOf(day), 2, RoundingMode.HALF_UP)表示小数点后保留两位,四舍五入。第三,整个计算方法必须加上@Transactional,因为工资计算是一个多步骤的批量操作,如果中间某一步出错了,前面插入的数据要全部回滚,不然就会产生脏数据。
4.3 统一返回结果与全局异常处理
写接口的时候一定要有一个统一的返回格式,不然前后端联调时你会疯掉。我习惯定义这样一个Result<T>类:
@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } public static <T> Result<T> error(Integer code, String message) { // 自定义错误码 } }配合@RestControllerAdvice做全局异常捕获,这样 Controller 里就不用到处写 try-catch 了。答辩的时候老师如果问“你的系统是怎么处理异常的”,你可以盯着当前项目的GlobalExceptionHandler,把@RestControllerAdvice的原理讲清楚,再把常见的业务异常如何映射到错误码说一遍,这一题就稳了。
4.4 定时任务:如何做到月底自动算薪
薪资系统有一个很自然的加分功能:定时任务。每个月 1 号凌晨自动计算上月工资,生成工资条。Spring Boot 里做定时任务非常简单,只要在启动类上加上@EnableScheduling,然后在方法上标注@Scheduled(cron = "0 0 1 1 * ?"),表示每月 1 号的 01:00:00 执行。
@Component public class SalaryScheduleTask { @Autowired private SalaryCalculateService salaryCalculateService; @Scheduled(cron = "0 0 1 1 * ?") public void autoCalculateSalary() { LocalDate lastMonth = LocalDate.now().minusMonths(1); salaryCalculateService.calculate( lastMonth.getYear(), lastMonth.getMonthValue() ); } }这里值得注意的坑是:定时任务不能重复执行。如果系统部署在多台机器上,或者同一台机器启动了多个实例,每个实例都会执行业务代码,导致工资数据重复计算。一般的解决方案是加一个分布式锁,但毕设场景下最简单的方案就是用数据库唯一键约束——工资明细表上加一个(emp_id, month)的唯一索引,重复插入会报错,在代码里捕获一下就行。
5. 实操全过程记录:从空项目到运行成功
5.1 环境准备:JDK、Maven、IDEA 的版本匹配
很多同学项目跑不起来的第一个原因就是环境版本不匹配。这里我列一个我自己实测没问题的组合:
| 工具 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8(即 8) | 启动 Spring Boot 2.x 最稳的版本 |
| Maven | 3.8.x | 3.9 也可以,但 3.8 更常见 |
| IDEA | 2021.x 以上 | 社区版也能用,但专业版功能更全 |
| MySQL | 5.7 或 8.0 | 8.0 注意驱动要选com.mysql.cj.jdbc.Driver |
Spring Boot 3.x 需要 JDK 17 以上,如果看到启动报错“Unsupported class file major version”,大概率就是 JDK 版本不对。遇到版本问题,优先检查java -version和mvn -version,确认两个命令指向的版本一致。IDEA 里可以打开Project Structure重新指定 JDK 路径和项目语言级别。
5.2 数据库初始化与配置
新建一个salary_db数据库,把表结构脚本导进去后,application.yml配置如下:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/salary_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: id-type: autotime-zone和jackson的时间格式这两个配置特别容易漏。不配的话,数据库返回的DATETIME类型传到前端会变成一串时间戳或者少 8 个小时,答辩论到时候你就会发现页面上的时间全是乱的。map-underscore-to-camel-case一定要设为 true,这样数据库里的emp_no字段就能自动映射到 Java 里的empNo属性,不用写一堆@TableField注解。
启动项目后可以在日志里看到 MyBatis 打印的 SQL 语句。这一步很有用,因为你可以直观地看到每次请求实际执行了什么 SQL,定位“数据查不到”或者“数据多出来”的问题。生产环境里一般会关掉这个日志,但毕设阶段建议开着。
5.3 前端项目的运行与联调
Vue 项目的启动比较简单,进入前端目录执行npm install安装依赖,然后npm run serve启动开发服务器。前后端联调的时候会遇到一个跨域问题——前端跑在 8081,后端跑在 8080,浏览器默认不允许这种跨域请求。解决办法是在后端加一个 CORS 配置类:
@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }加了 CORS 配置之后,前端调用后端接口就不会被浏览器拦截了。但你要清楚一个概念:跨域问题只存在于浏览器环境,Postman 里测试接口是不会有跨域的。所以在联调的时候,优先用 Postman 验证后端接口是否正常,然后再去排查前端的问题,不要混在一起排查。
5.4 项目打包:如何打成可运行的 Jar 包
最后的部署环节,我建议使用 Maven 的 package 命令打成 Jar 包。在 IDEA 右侧的 Maven 面板里双击package,或者在命令行执行mvn clean package -DskipTests,完成后在target目录下会生成一个salary-system-0.0.1-SNAPSHOT.jar。
把这个 Jar 包复制到一台装了 JDK 8 的电脑上,在命令行执行java -jar salary-system-0.0.1-SNAPSHOT.jar,等待几秒,看到Started Application的日志,整个系统就跑起来了。如果前端 Vue 项目也已经构建并放到了static目录下,直接访问http://localhost:8080就能看到完整的系统界面。
打包过程中容易踩的坑有两个。第一,前端文件要放在src/main/resources/static下再执行打包,不要先打包再把文件塞进 Jar 包——那样文件不会生效。第二,打包后的 Jar 包如果找不到数据源,多半是application.yml被 IDEA 的target/classes缓存了旧配置,执行一下mvn clean再重新 package。
6. 毕设开发中的高频问题与解决实录
6.1 Spring Boot 版本太高导致的启动失败
这个问题在毕设群里出现的频率极高。Spring Boot 3.x 发布之后,很多同学图新鲜直接用了 3.2,然后发现项目启动报错,上网一搜全是老版本的解决方案,对不上号。
最简单的解决办法是:回到 Spring Boot 2.7.x。如果你的项目已经用了 3.x 的代码,也不用推倒重来,改一下pom.xml中的parent版本号,再调整一下依赖的坐标即可。MyBatis-Plus 的 starter 到了新版本里改名了,如果你用的是mybatis-plus-boot-starter,在 3.x 里需要换成mybatis-plus-spring-boot3-starter,这个细节很容易忽略。
6.2 端口被占用
启动时如果看到Port 8080 was already in use,说明 8080 端口被其他进程占了。Windows 上执行:
netstat -ano | findstr 8080拿到 PID 之后,在任务管理器里结束对应进程,或者执行taskkill /PID 1234 /F强制结束。如果不是很严重的端口冲突,也可以在application.yml里改一个端口号,比如 8081,避免占用冲突。
6.3 金额计算出现 0.30000000000000004 这种数据
这是所有做支付、薪资、财务系统的人都会遇到的问题。原因在于浮点数在计算机里是用二进制存储的,0.1 + 0.2的结果并不是精确的0.3。解决办法只有一个:涉及金额的字段一律使用BigDecimal,并且通过字符串构造函数创建。
BigDecimal amount = new BigDecimal("3000.50"); // 正确 BigDecimal wrong = new BigDecimal(3000.50); // 错误另外,数据库里金额字段应该用DECIMAL(10,2),不要用FLOAT或者DOUBLE。前端拿到金额之后也尽量不要做加减运算,如果需要展示,直接原样呈现,计算逻辑都放到后端完成。
6.4 事务不生效的几种可能性
薪资计算这种多步骤操作必须加事务,但有时候你明明加了@Transactional,数据却还是没有回滚。常见原因有三个:
第一,方法被 private 修饰。Spring 的声明式事务是基于 AOP 代理的,代理只能拦截 public 方法,如果是 private 方法,@Transactional直接失效。第二,同类内部调用。同一个类里的 A 方法调用 B 方法,B 方法上的@Transactional不生效,因为调用发生在对象内部,没有经过代理对象。第三,异常被捕获了。事务回滚默认只在抛出RuntimeException时触发,如果代码里捕获了异常并做了处理,事务就感知不到错误。
这里简单提一句代理机制:Spring Boot 默认使用的是 CGLIB 代理,它会生成一个目标类的子类来覆盖方法。所以使用@Transactional的方法不能是final的,否则无法生成代理子类。这个话题如果深入讲可以做一篇完整的博客,但毕设阶段你只需要记得上面这三个坑就行。
6.5 前端页面表格数据绑不上,查接口返回 null
联调的时候经常遇到这种情形:后端接口返回的数据在 Postman 里是正常的,但前端表格里就是显示不出来。排查思路是先看控制台报什么错,然后在浏览器开发者工具里切到 Network 选项卡,找到对应请求,查看响应体。
常见的原因有两种:一种是后端返回的字段名和前端表格模板里的字段名对不上,比如后端是empNo,前端写的是empNo但是组件绑定的是emp_no。第二种是数据确实是 null,说明后端的“查询关联”没做好。查一下后端日志里打印的 SQL,看 join 条件是不是失效了,或者外键对应的数据不存在了,数据查出来就是空的。
这种问题我在做部门经理查看本部门员工薪资的时候踩过。当时前端传过来的是部门 ID,后端的 SQL 里员工表添加了dept_id作为外键,但新导入的测试员工数据没有写dept_id,查出来的薪资列表就只有几个人。后来把测试数据补齐,问题就解决了。给你的建议是:先造好完整的测试数据,再开始联调,不要边联调边造数据,问题会混在一起。
7. 论文撰写与答辩准备:最后一公里的关键动作
7.1 论文框架怎么搭
毕设论文不像技术博客,它有一套约定俗成的框架。薪资管理系统这篇论文,我的建议是:绪论 → 相关技术介绍 → 系统需求分析 → 系统设计 → 系统实现 → 系统测试 → 总结与展望。这套结构几乎适用于所有管理系统类毕设,照着写不会出大错。
需求分析部分要注意:不要只写“本系统需要员工管理功能”,要写“本系统需要支持员工的入职、离职、转岗操作,需要记录员工的部门调动历史,需要支持按照部门进行员工信息的筛选”。需求描述越具体,越说明你做了认真的调研,答辩时也更容易回答老师关于“你的系统解决了什么问题”的追问。
系统设计部分,除了数据库表设计,最好画清楚用例图、E-R 图、系统架构图。这步要求你会用工具画图,不要求你画得多专业,但用例图画清楚设备管理、薪资项目维护、工资条查看这些核心流程,会让整个论文加分不少。
7.2 答辩前必须模拟的 5 个问题
经验之谈:答辩的时候老师问的问题基本都是围绕这几类,提前准备答案,现场就不会卡壳。
“你的薪资计算流程是什么?”——按“薪资项目配置 → 考勤汇总 → 应发工资计算 → 扣除社保公积金 → 计算实发工资 → 生成工资条”这个顺序讲,最好配合流程图,讲清楚每一步的数据来源。
“你用了几个表?为什么这么设计?”——这个问题就看你对数据库设计的理解。答出来每个表的核心用途和表之间的关联关系,再把“薪资项目与员工解耦”的设计亮点讲出来,就能拿分。
“权限控制是怎么做的?”——回答用拦截器实现,分管理员、部门经理、普通员工三级权限,并说明数据权限在后端进行了二次校验,防止越权。
“项目有什么不足?”——这个问题如果答“没有不足”反而危险。你可以说“当前系统没有接入人脸识别打卡,考勤数据依赖手动录入;个税算法没有完全覆盖最新政策;未来可以引入薪资趋势分析和电子签收功能”。这样的回答既坦诚质量,又暗示了你思考过系统的演进方向。
“你用什么版本呢?为什么选 Spring Boot 2.7 而不是 3.x?”——答出来版本选择的原因是考虑第三方组件兼容性和社区资料丰富程度,能反映出你调研过。
7.3 最后再分享一个小经验
做完这个项目之后,我最深的感受是:毕设项目的难点从来不在技术,而在业务梳理和自我管理。技术上的问题,网上一搜基本都有答案,最难的是你能不能静下心来,把每一个模块的逻辑走通,把每一个异常场景处理掉。薪资管理系统这种题目,不像人工智能那么显眼,也不像电商系统那么大众,但它的业务足够清晰,做完之后你对数据库设计、事务机制、权限控制这些专业知识的理解,会远超那些只会复制粘贴代码的同学。
如果你正在纠结选题,或者已经开始写代码但被某个问题卡住了,我的建议就是坚持把这个题做完。做到“能讲清楚为什么这么设计,能演示出来,能回答上老师的追问”,这个项目就算成功了。