简介一套基于Java Swing与MySQL的员工工资管理系统面向正在学习Java桌面开发与数据库编程的初学者及在校学生帮助理解角色权限、业务逻辑与界面交互的完整实现过程。系统内置管理员与普通用户两类角色覆盖员工信息维护、部门管理、工资设置与查询、统计报表等常用模块功能层次清晰适合作为课程设计或毕业设计的参考蓝本。从员工新增、修改、删除到部门调整与薪资统计各业务模块均配有对应窗体与处理逻辑能够直观看到Swing组件布局、事件响应以及MySQL数据读写在实际项目中的组织方式。资源包共163个文件以Java源文件、窗体设计文件和编译后的class文件为主体另含MySQL数据库脚本、项目配置及可运行的jar包整体大小约944KB结构紧凑且易于导入调试。目前已有3332人学习下载源码注释与模块划分便于逐个功能研读可用于快速搭建同类管理系统的开发框架。1. Java 员工工资管理系统一个把 CRUD、权限、算薪和报表全装进去的练手项目Java 员工工资管理系统是课程设计和毕业设计里出现频率极高的题目也是很多 Java 工程师求职时拿来补项目经验的经典案例。它的难点不在「增删改查」而在「算薪」和「权限」同一套工资数据管理员要能看全公司、能批量核算、能导出发放员工却只能看到自己的工资条多一条别人的数据都不行。这个项目把 Java 后端开发的几块硬功夫——数据库建模、事务控制、金额精度、行级权限、Excel 导出——全都压在一个看似简单的业务里。适合三类人准备交课程设计的学生、想往简历上补一个完整项目的 Java 求职者以及需要快速搭一套内部薪酬应用的小团队。2. 技术选型与数据库设计Spring Boot MyBatis Plus 怎么搭才不返工2.1 技术栈取舍为什么直接用 Spring Boot 而不是 JSP Servlet很多课程设计教材还在教 JSP Servlet JDBC 三段式但一线开发里这套组合已经被淘汰得差不多了。我一般建议直接用 Spring Boot 2.7.x JDK 1.8 MySQL 8.0 MyBatis Plus 3.5.x。理由很实际Spring Boot 内嵌 Tomcat本地跑起来不用单独装服务器答辩演示时少一个环境变量问题MyBatis Plus 把单表 CRUD 的 SQL 省了你只需要专注写核算逻辑和权限控制工作量能砍掉三分之一。如果机器上有多个 JDK记得把JAVA_HOME指到 1.8 对应的目录IDEA 的 Project Structure 里也要把 Project SDK 和 Language level 对齐否则会出现「代码编译级别不对导致启动报错」这种莫名其妙的问题。pom.xml 里的核心依赖如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies注意mybatis-plus-boot-starter这个坐标只适用于 Spring Boot 2.x。如果你选了 Spring Boot 3.x必须换成mybatis-plus-spring-boot3-starter否则启动时会报 MyBatis 相关 Bean 找不到。MySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver连接串里要带serverTimezoneAsia/Shanghai老一代的com.mysql.jdbc.Driver已经移除了写错就是启动失败。2.2 四张核心表员工、工资、考勤、用户表用 DECIMAL 而不是 double数据库设计直接决定后面写核算逻辑的复杂度。我的习惯是拆四张表员工表存基本信息工资表存每个月的核算结果考勤表提供核算的原始数据用户表管登录和角色。工资表是核心主表字段要覆盖「应发构成、扣除构成、实发结果」三段。CREATE TABLE emp ( emp_id BIGINT NOT NULL AUTO_INCREMENT COMMENT 员工ID, emp_no VARCHAR(20) NOT NULL COMMENT 工号, emp_name VARCHAR(50) NOT NULL COMMENT 姓名, dept_id BIGINT NOT NULL COMMENT 部门ID, position_name VARCHAR(50) NOT NULL COMMENT 岗位, base_salary DECIMAL(10,2) NOT NULL COMMENT 基础工资, hire_date DATE NOT NULL COMMENT 入职日期, status TINYINT NOT NULL DEFAULT 1 COMMENT 1在职 0离职, PRIMARY KEY (emp_id), UNIQUE KEY uk_emp_no (emp_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT员工表; CREATE TABLE salary ( id BIGINT NOT NULL AUTO_INCREMENT, emp_id BIGINT NOT NULL COMMENT 员工ID, month VARCHAR(7) NOT NULL COMMENT 工资月份 2026-06, base_salary DECIMAL(10,2) NOT NULL COMMENT 基础工资, post_salary DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 岗位工资, performance DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 绩效, overtime_pay DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 加班费, allowance DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 补贴, social_security DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 五险一金个人部分, tax DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 个人所得税, gross_salary DECIMAL(10,2) NOT NULL COMMENT 应发工资, net_salary DECIMAL(10,2) NOT NULL COMMENT 实发工资, status TINYINT NOT NULL DEFAULT 0 COMMENT 0草稿 1已发放, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_salary_emp_month (emp_id, month), KEY idx_salary_month (month) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT工资表; CREATE TABLE attendance ( id BIGINT NOT NULL AUTO_INCREMENT, emp_id BIGINT NOT NULL, month VARCHAR(7) NOT NULL, work_days DECIMAL(5,2) NOT NULL COMMENT 出勤天数, overtime_hours DECIMAL(5,2) NOT NULL DEFAULT 0 COMMENT 加班小时, PRIMARY KEY (id), UNIQUE KEY uk_att_emp_month (emp_id, month) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考勤表; CREATE TABLE sys_user ( id BIGINT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL COMMENT BCrypt加密, emp_id BIGINT NULL COMMENT 员工ID管理员可为空, role VARCHAR(20) NOT NULL COMMENT ADMIN / EMPLOYEE, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;两个设计细节值得说明。第一工资月份用VARCHAR(7)而不是DATE存2026-06这种字符串。日期类型带时分秒一旦写代码时用错格式化模式很容易把 6 月 30 日的记录弄到 7 月去字符串直接比较反而最不容易错。第二所有金额字段全部用DECIMAL(10,2)实体类对应BigDecimal。用double存工资是新手最容易犯的错后面避坑章节我会专门讲精度问题。2.3 从实体类生成建表 SQLMyBatis Plus 官方不提供我这样处理「MyBatis Plus 根据 Java 实体类生成创建表的 SQL 语句」这个问题经常被问到先给结论MyBatis Plus 官方没有这个能力代码生成器生成的是实体类、Mapper、Service不是 DDL 脚本。如果你不想手写建表 SQL常见的做法有两类。一类是引入 Flyway把 SQL 脚本放进resources/db/migration里由应用启动时自动执行这是生产环境的标准做法另一类是写一个简单的工具反射读取实体类上的TableName、TableId、TableField注解拼接出 CREATE TABLE 语句。我写过一个简化版工具核心逻辑长这样public class DdlGenerator { public static String generate(Class? entityClass) { TableName tn entityClass.getAnnotation(TableName.class); StringBuilder sb new StringBuilder(CREATE TABLE ) .append(tn.value()).append( (\n); for (Field field : entityClass.getDeclaredFields()) { TableField tf field.getAnnotation(TableField.class); if (field.isAnnotationPresent(TableId.class)) { sb.append( ).append(field.getName()) .append( BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,\n); continue; } sb.append( ).append(tf.value()).append( ) .append(mapType(field.getType())).append(,\n); } sb.deleteCharAt(sb.length() - 2) .append() ENGINEInnoDB DEFAULT CHARSETutf8mb4;); return sb.toString(); } private static String mapType(Class? type) { if (type BigDecimal.class) return DECIMAL(10,2); if (type LocalDate.class) return DATE; if (type String.class) return VARCHAR(50); return VARCHAR(20); } }这段工具的本质是「用注解当元数据反射拿字段名和类型再映射成数据库类型」。需要注意TableField(emp_no)这种显式指定列名的场景还有字段名是驼峰、列名是下划线的转换逻辑上面为了精简没写全。课程设计或毕设用这种工具省事但如果是给公司做项目我建议老老实实上 FlywaySQL 脚本进版本库谁改了什么一清二楚。3. 工资核算核心逻辑个税、五险一金与 BigDecimal 金额精度3.1 先拆工资项目应发和扣除到底由哪些构成写核算代码之前先要在一张表里把工资构成列清楚。应发工资通常由基础工资、岗位工资、绩效、加班费、补贴五部分相加扣除部分主要是五险一金个人缴纳额和个税。应发减扣除得到实发。这个顺序不能乱五险一金的计算基数是「当月应发工资」而不是「实发工资」而个税的应纳税所得额是「应发工资 - 五险一金个人部分 - 专项附加扣除」。各个项目的计算口径如下工资项目计算口径备注基础工资员工表直接取值固定值按月发放岗位工资按岗位等级配置可以做成配置表先写死在常量类绩效绩效分数 × 绩效基数课程设计可直接按比例折算加班费加班小时 × 时薪时薪 基础工资 / 21.75 / 8五险一金个人部分基数 × 比例养老 8%医疗 2%公积金 5%-12%个税累计预扣法应发 - 社保后按税率表分档这里唯一有争议的是五险一金的基数。真实场景里基数有上下限下限是当地平均工资的 60%上限是 300%而且每年调整一次。课程设计不必把规则做进代码可以直接拿基础工资当基数但注释里要写清楚「这是简化处理」答辩时反而能体现你明白真实业务的复杂度。3.2 用 BigDecimal 计算个税按月预扣的简化实现个税计算是工资系统里最容易写错又最值得写的逻辑。真实场景用累计预扣法要维护每个员工当年的累计收入和累计已纳税额。课程设计为了控制复杂度可以简化为按月计算但税率表要按最新的七级超额累进税率来。为什么不建议课堂作业里的double因为浮点数在二进制里表示不精确0.1 0.2的结果是0.30000000000000004。工资算错一分钱财务都不会放过你。public class TaxCalculator { private static final BigDecimal[] LEVELS { new BigDecimal(3000), new BigDecimal(12000), new BigDecimal(25000), new BigDecimal(35000), new BigDecimal(55000), new BigDecimal(80000) }; private static final BigDecimal[] RATES { new BigDecimal(0.03), new BigDecimal(0.10), new BigDecimal(0.20), new BigDecimal(0.25), new BigDecimal(0.30), new BigDecimal(0.35), new BigDecimal(0.45) }; private static final BigDecimal[] DEDUCTIONS { new BigDecimal(0), new BigDecimal(210), new BigDecimal(1410), new BigDecimal(2660), new BigDecimal(4410), new BigDecimal(7160), new BigDecimal(15160) }; public static BigDecimal calcTax(BigDecimal taxableIncome) { if (taxableIncome.compareTo(BigDecimal.ZERO) 0) { return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP); } for (int i LEVELS.length - 1; i 0; i--) { if (taxableIncome.compareTo(LEVELS[i]) 0) { return taxableIncome.multiply(RATES[i 1]) .subtract(DEDUCTIONS[i 1]) .setScale(2, RoundingMode.HALF_UP); } } return taxableIncome.multiply(RATES[0]) .setScale(2, RoundingMode.HALF_UP); } }这段代码用的是「超额累进税率速算扣除数」算法先从高往低找应纳税所得额落在哪一档然后乘以对应税率再减去速算扣除数。乘法和减法都用 BigDecimal最后统一setScale(2, RoundingMode.HALF_UP)保留两位小数四舍五入的规则和财务口径一致。注意new BigDecimal(3000)一定要传字符串直接new BigDecimal(3000)虽然这个数恰好是精确的但带小数的金额直接传 double 会带上浮点误差。3.3 核算主流程从考勤数据到工资单核算逻辑放在 Service 层一次调用要完成「查员工、查考勤、逐项算钱、落库」整个流程。我的写法是把单个员工的核算拆成独立方法批量核算时循环调用既方便单条重算也方便排查问题。Service RequiredArgsConstructor public class SalaryService { private final EmpMapper empMapper; private final AttendanceMapper attendanceMapper; private final SalaryMapper salaryMapper; Transactional(rollbackFor Exception.class) public void generateMonthlySalary(String month) { ListEmp empList empMapper.selectList( new LambdaQueryWrapperEmp().eq(Emp::getStatus, 1)); for (Emp emp : empList) { generateOne(emp, month); } } Transactional(rollbackFor Exception.class) public void generateOne(Emp emp, String month) { Long exist salaryMapper.selectCount(new LambdaQueryWrapperSalary() .eq(Salary::getEmpId, emp.getEmpId()) .eq(Salary::getMonth, month)); if (exist 0) { return; // 当月已核算跳过保证幂等 } Attendance att attendanceMapper.selectOne( new LambdaQueryWrapperAttendance() .eq(Attendance::getEmpId, emp.getEmpId()) .eq(Attendance::getMonth, month)); BigDecimal base emp.getBaseSalary(); BigDecimal post new BigDecimal(0); BigDecimal perf new BigDecimal(0); BigDecimal overtime BigDecimal.ZERO; if (att ! null) { BigDecimal hourlyRate base.divide(new BigDecimal(21.75), 4, RoundingMode.HALF_UP) .divide(new BigDecimal(8), 4, RoundingMode.HALF_UP); overtime hourlyRate.multiply(att.getOvertimeHours()) .setScale(2, RoundingMode.HALF_UP); } BigDecimal gross base.add(post).add(perf).add(overtime); BigDecimal socialSecurity gross.multiply(new BigDecimal(0.105)) .setScale(2, RoundingMode.HALF_UP); BigDecimal tax TaxCalculator.calcTax(gross.subtract(socialSecurity)); BigDecimal net gross.subtract(socialSecurity).subtract(tax); Salary salary new Salary(); salary.setEmpId(emp.getEmpId()); salary.setMonth(month); salary.setBaseSalary(base); salary.setPostSalary(post); salary.setPerformance(perf); salary.setOvertimePay(overtime); salary.setSocialSecurity(socialSecurity); salary.setTax(tax); salary.setGrossSalary(gross); salary.setNetSalary(net); salary.setStatus(0); salaryMapper.insert(salary); } }这段代码的关键在幂等检查先查当月是否已有记录有就跳过。这样同一个月份跑两次核算不会产生重复工资单也方便「先撤回复核再重新生成」的操作。时薪计算里divide必须指定精度和舍入方式我用了 4 位小数最后乘完加班小时再保留 2 位避免中间步骤的舍入误差累积到肉眼可见。五险一金比例先按 10.5% 的简化值写真实比例应该从配置读取这一点放到第 6 章展开。3.4 批量核算的事务与性能saveBatch 要不要用前面generateMonthlySalary里循环调用generateOne每个员工单独 insert1000 人就是 1000 次插入。对课程设计无所谓但如果员工上万性能会明显变差。MyBatis Plus 提供了saveBatch方法本质是把多条 INSERT 拼成一条多值语句减少网络往返。改造方式很简单核算时先不落库把 Salary 对象装进 List最后统一saveBatch。但要注意事务边界也变了——循环 5000 人时其中第 3000 号人算错了整个批次回滚数据库没有任何记录这是对的。可如果中途调用了外部接口发通知通知发出去又回滚不了这就产生了「本地事务和外部操作不一致」的问题。哪怕只有两三条通知也建议发通知的动作放在事务提交之后用TransactionSynchronizationManager.afterCommit触发。4. 双角色权限管理员与员工的分权访问怎么实现4.1 权限需求拆解菜单权限与行级权限是两回事工资系统的权限要分两层看。首先是菜单权限管理员能进「工资核算」「员工管理」「发放确认」这些页面员工只能进「我的工资条」。其次是行级权限同样是查询工资接口管理员传empId88能查到 88 号员工的记录员工传empId88必须被拒绝。行级权限是面试官最爱的追问点也是真实系统里最容易出越权漏洞的地方。实现上我推荐拦截器 Session不引入 Spring Security。不是 Security 不好而是对一个课程设计级别的项目Security 的过滤器链和配置项反而让你花大量时间在「配权限」而不是「写业务」上。密码加密单独引入spring-security-crypto里的 BCrypt 就够了。4.2 用拦截器 Session 做最小可用的登录校验拦截器是 Spring MVC 里做登录校验最轻量的方式。登录成功把用户信息放进 Session之后每个请求进 Controller 之前拦截器先检查 Session 里有没有用户没有就直接重定向到登录页。Component public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object user request.getSession().getAttribute(loginUser); if (user null) { response.sendRedirect(/login); return false; } return true; } }Configuration public class WebConfig implements WebMvcConfigurer { Resource private LoginInterceptor loginInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns(/**) .excludePathPatterns(/login, /login/**, /css/**, /js/**); } }注册拦截器时addPathPatterns(/**)表示拦截所有请求excludePathPatterns放行登录页和静态资源。两个容易踩的细节Session 的 key 要统一定义成常量避免登录时存loginUser、拦截器取user这种对不上的问题loginUser对象里除了用户名还要放role和empId后面行级权限要用。4.3 行级权限员工接口不信任前端传的 id很多新手写员工查询工资接口时是从请求参数里拿empId的。这等于把权限判断全交给了前端——攻击者用浏览器开发者工具改一下empId就能看到别人的工资条。正确的做法是员工角色查询时empId一律从 Session 里取管理员角色才允许传empId并且要有额外的管理员角色校验。GetMapping(/salary/self) public Result getMySalary(String month) { LoginUser user (LoginUser) session.getAttribute(loginUser); if (!EMPLOYEE.equals(user.getRole())) { return Result.error(该接口仅限员工访问); } Salary salary salaryMapper.selectOne(new LambdaQueryWrapperSalary() .eq(Salary::getEmpId, user.getEmpId()) // 从 Session 取不信任前端参数 .eq(Salary::getMonth, month)); return Result.ok(salary); }GetMapping(/salary/admin) public Result getSalaryByEmp(Long empId, String month) { LoginUser user (LoginUser) session.getAttribute(loginUser); if (!ADMIN.equals(user.getRole())) { return Result.error(无权限); } Salary salary salaryMapper.selectOne(new LambdaQueryWrapperSalary() .eq(Salary::getEmpId, empId) .eq(Salary::getMonth, month)); return Result.ok(salary); }两个接口对比着看核心原则是「服务端永远不信任前端传过来的身份信息」。Session 是登录时服务端下发的攻击者改不了请求参数是攻击者随便改的。这种越权漏洞在安全领域叫 IDOR不只在工资系统里任何多租户系统都要防这一手。管理员的empId参数可以传但必须校验角色是 ADMIN否则任何人登录后都能通过这个接口查全公司工资。5. 避坑指南工资管理系统最常见的 6 个坑与排查方法5.1 工资月份用 Date 还是 String时区偏移导致归属错误现象6 月 30 日生成的工资记录查出来月份变成 7 月。原因数据库字段用的是datetimeJava 端用SimpleDateFormat(YYYY-MM)格式化——注意YYYY是「周年份」yyyy才是自然年份周跨年时YYYY会直接把日期归到错误年份另外datetime带时分秒前端展示时一不小心就把 6 月 30 日 00:00 显示成别的样子。解决工资月份字段直接用VARCHAR(7)存2026-06字符串Java 端用YearMonth.now().toString()生成完全不经过日期格式化。这个方案排序正确、索引友好、跨数据库通用。5.2 金额用 double 计算工资条出现一堆小数现象核算完的实发工资是5000.300000000001数据库 DECIMAL 字段存不进去直接报错。原因double用二进制表示十进制小数天生有误差0.1 0.2都不等于0.3。解决实体类金额字段全部用BigDecimal数据库用DECIMAL(10,2)运算时注意两点——不要new BigDecimal(0.1)而要new BigDecimal(0.1)前者拿到的仍是浮点数的二进制近似值除法必须指定精度和舍入模式divide(BigDecimal.ONE, 2, RoundingMode.HALF_UP)否则抛 ArithmeticException。这条规则不是可选项是必须项。5.3 Excel 导出乱码与日期列变成数字现象用 POI 导出工资单文件名在浏览器里变成%E5%B7%A5%E8%B5%84.xls打开后日期列是一串像45000的数字。原因响应头里Content-Disposition的 filename 没有做 URL 编码中文文件名在传输过程中变成乱码日期单元格没有设置日期格式POI 默认把日期当数值写入 Excel。解决思路是设置响应头用URLEncoder.encode编码文件名导出时对日期字段显式设置单元格格式。用 EasyExcel 比原生 POI 省心字段加DateTimeFormat(yyyy-MM-dd)注解即可但也一样要正确设置响应头。GetMapping(/salary/export) public void export(String month, HttpServletResponse response) throws Exception { String fileName URLEncoder.encode(工资_ month, UTF-8); response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment; filename fileName .xlsx); ListSalaryExcelVO list salaryMapper.selectSalaryWithEmp(month); EasyExcel.write(response.getOutputStream(), SalaryExcelVO.class) .sheet(工资条) .doWrite(list); }乱码问题的根源在 HTTP 响应头只支持 ASCII 字符中文文件名必须编码后才能放进Content-Disposition。URLEncoder.encode之后浏览器能正确解码这就是为什么导出的文件名是中文、而不是%E5%B7%A5%E8%B5%84的原因。5.4 事务不生效同类调用和 try-catch 吞异常现象批量核算时第 50 个员工的数据写入失败抛了异常但前面 49 个员工的数据没有回滚数据库里留了半批数据。原因Transactional依赖 Spring 的 AOP 增强机制同一个类内部this.generateOne()这种调用不会经过增强入口注解直接失效另一个常见原因是核算方法内部把异常try-catch吞掉了事务管理器感知不到异常自然不触发回滚。解决把核算方法拆到另一个 Service 类中通过注入的实例调用异常要么不 catch 要么 catch 后重新抛出RuntimeException。如果实在要同类调用用AopContext.currentProxy()获取当前类的代理对象再调用但代码可读性会变差。5.5 启动失败端口占用、JDK 版本不匹配、数据库驱动缺失现象启动 Spring Boot 应用控制台报Port 8080 was already in use或者Failed to configure a DataSource。原因前者是本地另一个进程占用了 8080后者分两种情况——pom 里没引入 MySQL 驱动或者application.yml的driver-class-name写成了过时的com.mysql.jdbc.Driver。排查顺序很固定先netstat -ano | findstr 8080Windows或lsof -i:8080Linux/macOS查端口再java -version和mvn -version核对 Java 版本是否和编译级别一致最后看依赖里有没有mysql-connector-j。还有一个 IDEA 专属问题项目编译时内存溢出报java.lang.OutOfMemoryError可以在 IDEA 的 Help 菜单里改 VM 参数把-Xmx调到 2048m 以上32 位 JDK 认不了超过 1.5G 的堆。5.6 MyBatis Plus 分页插件没配置查第二页还是第一页的数据现象调用Page分页查询getTotal()返回了全表数量但getRecords()每次返回相同的数据第二页和第一页一样。原因MyBatis Plus 的分页功能依赖PaginationInnerInterceptor插件没有注册这个拦截器时Page对象只是把LIMIT参数拼进去了但 SQL 执行时根本没有拼接 LIMIT 语句。解决新增一个 MyBatis Plus 配置类。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(500L); interceptor.addInnerInterceptor(pagination); return interceptor; } }setMaxLimit(500L)是给分页查询设置单页最大行数防止有人传一个size999999把整张表拉出来这是性能保护和数据泄露的双重防线。如果管理系统里的工资表有几十万条这个参数能帮你挡掉不少无意的全表查询。这个坑非常隐蔽因为不报错只是数据不对没经验的人会以为是Page用错了。6. 让工资规则可配置把写死的计算公式抽成规则引擎前面所有核算代码里五险一金比例、绩效系数都是写死在代码里的。真实项目里这些东西每年都可能变社保基数调整、个税起征点变化、公司新加一项补贴。每次改规则都要改代码、重新打包发布运维和财务都会疯掉。所以最后一个进阶技巧是把工资项目抽成配置表让核算引擎从配置里读取规则。配置表结构可以这样设计CREATE TABLE salary_item_config ( id BIGINT NOT NULL AUTO_INCREMENT, item_code VARCHAR(30) NOT NULL COMMENT 项目编码 BASE_SALARY, item_name VARCHAR(50) NOT NULL COMMENT 项目名称 基础工资, cal_type TINYINT NOT NULL COMMENT 0固定金额 1按比例 2按公式, cal_value VARCHAR(100) NOT NULL COMMENT 固定值或比例 0.08, sort_order INT NOT NULL DEFAULT 0 COMMENT 计算顺序, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT工资项目配置表;核算引擎遍历配置表按cal_type分派到不同的计算策略cal_value里的值从数据库读出来直接参与公式计算。我见过太多人把个税税率表写死在 Service 里等到政策调整时连夜改代码、跑测试、发版本整个过程提心吊胆。后来我就学乖了凡是有可能变的数字一律进配置、进数据库让规则成为数据的一部分而不是代码的一部分。代码里留一张策略分派表加新工资项目只需要 insert 一条配置记录不用动 Java。配置化的代价是多了两张表、一个加载逻辑但对于「做完这个项目要长期维护」的场景这笔账怎么算都划算。这个项目做到这一步其实已经从「课程设计」往「生产级薪酬系统」的方向迈了一大截。把规则配置化之后再去补发放记录管理、工资条签收、历史版本追溯这些模块就不会是结构性的返工只是往上叠加。希望这些从表结构到事务、从精度到越权的实战经验能帮到你少走几段我当年走过的弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?