资讯动态

SSM人力资源管理系统设计与实现:从数据库到部署避坑指南

发布时间:2026/10/5 7:17:10 来源:尧图企业网站定制
简介面向Java毕业设计、SSM框架学习以及中小企业人事信息化建设场景这份基于SSM与B/S架构的人力资源管理系统是一套功能完整、结构清晰、可直接运行调试的毕业设计项目。后端采用Spring、SpringMVC、MyBatis整合Maven前端使用JSP、CSS、jQuery支持管理员与普通用户两类角色覆盖登录注册、员工管理、上下班打卡、奖惩管理、绩效管理、工资管理、离职退休费用管理、培训管理、岗位工种管理、系统管理等核心模块前后台分层清晰适合课程设计、项目实训或二次开发。资源包为zip格式大小约18.01MB随附项目源码与MySQL数据库脚本并配有开题报告、毕业论文便于导入IDEA或Eclipse运行也为撰写毕设文档提供支撑。目前已有76人学习下载尤其适合需要从零搭建SSM人事系统、理解角色权限与业务闭环的Java学习者参考。1. SSM 人力资源管理系统为什么中小企业第一套信息化系统大多长这样一家 50 到 200 人的制造企业人事还在用 Excel 记考勤、算工资月底对不上账是常态。老板想上系统又怕大厂 OA 太重、太贵、改不动。这时候基于 SSMSpring SpringMVC MyBatis搭建的 B/S 架构人力资源管理系统就成了最常见的答案它不挑服务器一台 2 核 4G 的机器就能跑代码开源可改遇到特殊流程自己能调而且对 Java 开发者来说SSM 是面试和工作中绕不开的经典组合。这篇笔记我会从数据库设计、框架配置、核心业务实现到部署排错把整套系统的落地路径完整讲一遍新手能照着搭熟手可以直接跳到自己关心的章节看参数和踩坑记录。2. 先做模块拆解与数据库设计权限不清后面全是返工2.1 B/S 架构下的模块划分与角色模型人力资源管理系统和电商系统最大的区别在于它的核心不是交易而是状态流转。一个员工从入职、转正、调岗到离职状态一直在变考勤从打卡到汇总再到薪资计算是一条完整的数据链。所以第一步不是写代码而是把角色和模块的边界划清楚。我一般把系统拆成六大模块组织架构部门、岗位、员工管理档案、入职、调动、考勤管理排班、打卡、请假、薪资管理工资项、月度结算、系统管理用户、角色、菜单。角色方面中小企业通常只需要三种管理员、人事专员、普通员工。管理员管组织和权限人事专员操作考勤和薪资普通员工只查自己的信息和工资条。这里有个常见的分歧点要不要把「部门经理」单独作为一个角色我的建议是最早期不要。部门经理的审批流可以等系统跑稳了再加一开始就上多级审批页面、表结构、权限配置都会翻倍项目很容易烂尾。先用一张sys_role表撑三种角色接口层用注解做权限判断后续加角色只是加数据的事。2.2 数据库表设计五张核心表的字段取舍SSM 项目的数据库设计有个特点表不会太多二三十张封顶但每张表的字段必须贴近真实业务。我见过太多学生项目把员工表和用户表合并成一张最后登录、权限、离职复用全搅在一起。这里给出一套我常用的最小可用表结构可以直接导入 MySQL 使用。员工表emp_employeeCREATE TABLE emp_employee ( id bigint(20) NOT NULL AUTO_INCREMENT, emp_no varchar(20) NOT NULL COMMENT 工号, name varchar(50) NOT NULL COMMENT 姓名, gender tinyint(1) DEFAULT 1 COMMENT 1男 2女, dept_id bigint(20) DEFAULT NULL COMMENT 部门ID, position varchar(50) DEFAULT NULL COMMENT 岗位, entry_date date DEFAULT NULL COMMENT 入职日期, status tinyint(1) DEFAULT 1 COMMENT 1在职 2离职 3停薪留职, phone varchar(20) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_emp_no (emp_no), KEY idx_dept (dept_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明emp_no设置唯一索引是因为工号在中小企业的线下流程里就是唯一标识很多导出报表直接用工号做关联重复了后面查薪资会出大问题。status不是布尔值而是 tinyint因为离职和停薪留职是两种完全不同的状态布尔只能表达两种。dept_id建普通索引而不是唯一索引一个部门多个人是常态。部门表emp_dept字段更少id、dept_name、parent_id、manager_id。parent_id自关联用来支持树形结构manager_id指向员工表主键。这里注意不要用dept_level去硬编码层级中小企业组织架构最多三层递归查询的开销完全可以接受真到了要优化树结构的时候系统规模早就该换技术栈了。考勤表和薪资表是踩坑重灾区。设计考勤表时我建议把「每日汇总」和「打卡流水」分开。打卡流水表att_record记录每一次打卡时间每日汇总表att_daily存员工某天的上班时间、下班时间、迟到分钟数、请假类型。如果你只建一张流水表月底统计迟到次数时一条员工一天打卡四次的数据要写一堆嵌套查询性能差还容易算错。薪资表的核心是「薪资项拆分」。不要只存一个总工资要拆出基本工资、岗位工资、绩效、餐补、社保个人部分、实发合计每一项一个字段或者用子表存。我见过只存实发金额的系统财务对账时发现社保基数不对想追溯历史数据根本无从下手这就是黑匣子式设计的典型后果。2.3 数据库初始化脚本与数据字典数据库脚本建议随手写注释。SSM 项目给导师或面试官看源码时sql文件夹里一份带完整注释的建表脚本比代码更有说服力它直接证明你理解业务。脚本里除了建表语句还需要预置管理员账号。密码不要明文存用 MD5 加盐或者 BCrypt 加密后的字符串直接写进 INSERT 语句。数据字典方面我习惯把考勤状态、员工状态、请假类型这些枚举值单独建一张sys_dict表用dict_type和dict_value两个字段标识前端下拉框直接查这张表渲染。好处是以后加一个「婚假」「产假」不需要改代码人事在后台录入字典即可。这套做法在真正的企业项目里非常常见而很多自学 SSM 的开发者会忽略掉导致页面上的下拉选项全写在 JS 里改起来很痛苦。3. 搭建 SSM 骨架从 Maven 配置到第一个登录接口3.1 Maven 依赖与项目结构SSM 项目的搭建核心是让三大框架各司其职Spring 管 BeanSpringMVC 管请求分发MyBatis 管 SQL。用 Maven 管理依赖时最怕的是版本冲突。我的基线配置是Spring 5.2.x、MyBatis 3.5.x、MyBatis-Spring 2.0.x对应 JDK 1.8。这套组合经过大量项目验证兼容性最稳不要追求新版 Spring 6那要求 JDK 17很多中小企业服务器还在 JDK 8 上跑。!-- pom.xml 关键依赖 -- properties spring.version5.2.22.RELEASE/spring.version /properties dependencies dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.9/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.6/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.8/version /dependency /dependencies逻辑说明数据源用 Druid 而不是 Spring 内置的DriverManagerDataSource原因是 Druid 自带连接池监控在开发阶段能看到每个接口打开了多少连接、有没有泄漏。这在排查「系统跑两天就卡死」这类连接池耗尽问题时几乎是唯一高效的路径。项目结构按「控制层-服务层-持久层」三层分包controller、service、mapper加上entity或pojo、common放统一返回结果和异常处理、config放配置类。controller 只做参数接收和结果封装不要写业务逻辑。我见过很多项目把查询数据库的代码直接写在 controller 里当时觉得省事后来加一个缓存逻辑要改三个接口后悔药都买不到。3.2 Spring 与 SpringMVC 配置注解驱动替代 XML现在的 SSM 项目主流做法是 Java Config 搭配 XML 混合或者全注解。我倾向于保留一个spring.xml做数据源和事务管理SpringMVC 部分单独放在spring-mvc.xml。分文件的原因很简单项目大了以后Spring 容器和 SpringMVC 容器扫描的包要错开否则事务注解会失效。!-- spring.xml 核心配置 -- context:component-scan base-packagecom.example.hrms context:exclude-filter typeannotation expressionorg.springframework.stereotype.Controller/ /context:component-scan bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/hrms?useUnicodetrueamp;characterEncodingutf8amp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword valueyourpassword/ property namemaxActive value20/ property nameinitialSize value5/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nametypeAliasesPackage valuecom.example.hrms.entity/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean tx:annotation-driven transaction-managertransactionManager/注意url里的serverTimezoneAsia/Shanghai必须加MySQL 8 的驱动默认时区和本地不一致不加会在连接时报错。characterEncodingutf8解决中文乱码。maxActive20是个经验值中小企业系统并发不超过 5020 个连接足够设太大会浪费内存太小高峰期会报Connection is not available。SpringMVC 部分核心是开启注解驱动和静态资源放行。SSM 项目是前后端不分离的JSP 页面直接放在webapp/WEB-INF/views下所以 JS、CSS 文件要单独配置mvc:resources映射否则浏览器加载页面时 404。3.3 MyBatis 的坑点parameterType 与 resultMapMyBatis 的 Mapper 接口和 XML 映射文件之间的匹配规则接口全限定名必须等于 XML 文件的 namespace接口方法的名称必须等于 XML 里 statement 的 id。这个规则新手经常忽略报错信息却非常隐晦只会告诉你Invalid bound statement (not found)第一次遇到能卡一整天。!-- EmployeeMapper.xml -- mapper namespacecom.example.hrms.mapper.EmployeeMapper resultMap idEmployeeWithDept typeEmployee id propertyid columnid/ result propertyempNo columnemp_no/ result propertyname columnname/ association propertydept javaTypeDepartment id propertyid columndept_id/ result propertydeptName columndept_name/ /association /resultMap select idselectEmployeeWithDept resultMapEmployeeWithDept SELECT e.*, d.dept_name FROM emp_employee e LEFT JOIN emp_dept d ON e.dept_id d.id WHERE e.id #{id} /select /mapper逻辑说明resultMap里association用来处理员工和部门的多对一关系。查询时用LEFT JOIN而不是INNER JOIN因为离职员工的部门可能已经被删了内连接会把这条员工记录丢掉导致历史数据对不上。#{id}是预编译占位符MyBatis 会转成?做参数绑定不要用${id}那是字符串拼接有注入风险。MyBatis 的另一个重点是「驼峰映射」。在spring.xml里配置setting namemapUnderscoreToCamelCase valuetrue/这样数据库的emp_no能自动映射到实体类的empNo不需要手写resultMap的每个字段。但对多表关联查询我还是建议显式写resultMap可读性更好也方便加association。3.4 登录接口从 Controller 到 Session 的完整链路登录功能是全员通的关键路径几乎每个模块都要判断「当前用户是谁」。SSM 项目的标准做法是把登录用户存进 Session然后用拦截器做登录校验。代码上分三层写我们来看 Controller 层和拦截器的实现。Controller RequestMapping(/auth) public class AuthController { Autowired private UserService userService; PostMapping(/login) ResponseBody public Result login(RequestParam String username, RequestParam String password, HttpServletRequest request) { User user userService.login(username, password); if (user null) { return Result.fail(用户名或密码错误); } request.getSession().setAttribute(loginUser, user); return Result.success(user); } }逻辑说明ResponseBody把返回对象序列化成 JSON前端用 AJAX 接收。login方法在 Service 层里做密码校验注意这里 Service 返回的是User对象而不是布尔值因为后续跳转页面要展示用户名、角色一次查询把数据带出来比登录后再查一次更高效。Session 存对象而不是存 userId也是同样的道理。登录拦截器的配置在spring-mvc.xml里注册mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/auth/login/ mvc:exclude-mapping path/static/**/ bean classcom.example.hrms.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors拦截器类实现HandlerInterceptor接口在preHandle里判断 Session 是否存在loginUser不存在就重定向到登录页。这里有个细节AJAX 请求被拦截后如果返回 302 重定向前端拿到的是一段 HTML 而不是 JSON体验很差。我一般会在拦截器里判断请求头X-Requested-With如果值是XMLHttpRequest就直接返回 401 状态码前端统一处理跳转。4. 核心业务功能落地考勤统计与工资计算的数据流转4.1 考勤模块每日汇总的更新策略考勤是人力资源管理系统里最容易写出性能灾难的模块。假如员工一天打卡四次一个月 200 人就是 24000 条流水如果月底统计全部靠实时计算页面基本要转十几秒。我的做法是用一个定时任务每天凌晨汇总昨天的数据写入att_daily表月底查询只查汇总表速度在毫秒级。汇总任务的思路是查出前一天所有打卡流水按员工分组取最小时间为上班时间、最大时间为下班时间然后和排班表比较得出迟到、早退状态。这里有一个业务细节午饭时间要处理掉。如果员工 12:00 打一次下班卡、 13:00 打一次上班卡那么一天的流水是四条「迟到分钟数」必须扣除午休时段否则下午打卡稍微晚两分钟就会被算成迟到半小时。// 考勤每日汇总 - Service 层核心逻辑 public void dailyAttendanceSummary(String workDate) { ListMapString, Object flowList attendanceMapper.selectFlowByDate(workDate); MapLong, ListMapString, Object grouped flowList.stream() .collect(Collectors.groupingBy(m - (Long) m.get(empId))); for (Map.EntryLong, ListMapString, Object entry : grouped.entrySet()) { Long empId entry.getKey(); ListMapString, Object records entry.getValue(); records.sort(Comparator.comparing(m - (Timestamp) m.get(punch_time))); Timestamp first (Timestamp) records.get(0).get(punch_time); Timestamp last (Timestamp) records.get(records.size() - 1).get(punch_time); int lateMinutes calcLateMinutes(first, records); int earlyMinutes calcEarlyMinutes(last, records); AttendanceDaily daily new AttendanceDaily(); daily.setEmpId(empId); daily.setWorkDate(workDate); daily.setOnTime(first); daily.setOffTime(last); daily.setLateMinutes(lateMinutes); daily.setEarlyMinutes(earlyMinutes); attendanceMapper.upsertDaily(daily); } }逻辑说明groupingBy把流水按员工 ID 分组每个组内的记录按打卡时间排序。calcLateMinutes方法里传入的是整组记录而不是只传第一个时间原因是如果有员工上班打了一次卡下班忘了打第二次打卡会被当成上班时间处理需要用打卡次数和排班时段校准这里业务规则比较复杂建议在方法里多写注释。参数说明workDate是字符串格式yyyy-MM-dd控制层通过日期选择器传入。定时任务用 Spring 的Scheduled(cron 0 30 2 * * ?)每天凌晨两点半执行错开数据库备份时间戳。执行结果要留日志第二天发现数据异常时能查得到前一天的汇总任务是否跑成功。4.2 薪资模块动态 SQL 处理五险一金计算薪资计算是人事系统里最敏感也最容易翻车的功能。企业给员工的工资条和财务实际发放的工资中间隔着一堆扣除项社保个人缴纳部分、公积金、个税、缺勤扣款。SSM 里处理这种逻辑我一般用「底数规则」的方式底数基本工资、岗位工资从薪资表读取规则社保比例、个税起征点从配置表读取最后在 Service 层逐层叠加。个税计算在 2019 年之后改成累计预扣法很多早期项目还在用旧的按月计算逻辑会被人事指出来「个税算错了」。正确的逻辑是每月计算时取员工当年截至本月的累计收入、累计免税收入、累计专项扣除套用年度税率表计算累计应纳税额减去已预缴税款才得到本月应扣个税。这套逻辑代码量不小一个独立方法加 80 行左右注释写到每个税率的边界值。!-- 薪资月结查询使用动态 SQL 处理可选月份筛选 -- select idselectSalaryDetail resultTypemap SELECT e.emp_no, e.name, s.base_salary, s.post_salary, s.performance, s.social_security, s.housing_fund, s.tax, (s.base_salary s.post_salary s.performance - s.social_security - s.housing_fund - s.tax) AS actual_salary FROM emp_salary s LEFT JOIN emp_employee e ON s.emp_id e.id where if testmonth ! null and month ! AND s.salary_month #{month} /if if testdeptId ! null AND e.dept_id #{deptId} /if if testkeyword ! null and keyword ! AND (e.name LIKE CONCAT(%, #{keyword}, %) OR e.emp_no LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY e.dept_id, e.emp_no /select逻辑说明where标签配合if实现动态拼接用户选了多少条件就拼多少 SQL没选的条件自动忽略。这是 MyBatis 相较于 JDBC 手写拼接的最大优势也是面试中常问的细节。actual_salary在 SQL 层直接算出来Service 层不用再循环做减法性能开销少一点。注意这里LIKE CONCAT(%, #{keyword}, %)不要写成%${keyword}%后者存在注入风险且索引失效。薪资计算的时机也要考虑考勤汇总跑完后薪资计算才能开始因为缺勤扣款依赖考勤数据。我建议把它设计成手动触发而不是定时任务原因很现实——每次算工资前人事都要确认考勤有没有异常自动算了反而给人添乱。页面上给一个「生成工资单」按钮点击后 Service 层先检查考勤汇总表有没有最新日期的数据没有就提示「请先完成考勤汇总」这个防呆设计非常致命。4.3 员工模块批量导入与事务控制人事系统里还有一个高频操作是员工批量导入。50 人以下的公司可能用不上但 200 人的工厂年初入职一批人HR 一个一个录会录到崩溃。我实现的是 Excel 导入用户上传.xlsx文件后台用 POI 解析逐行校验最后批量插入。这里面事务控制很关键——如果 100 行数据有 3 行格式错误是全部回滚还是跳过错误行继续导入我的做法是格式错误直接跳过并记录原因插入失败全部回滚。事务的粒度在 Service 层的Transactional注解上控制。Transactional(rollbackFor Exception.class) public ImportResult importEmployees(MultipartFile file) { ListEmployeeImportDTO list ExcelUtil.parse(file); ImportResult result new ImportResult(); int successCount 0; for (EmployeeImportDTO dto : list) { if (validate(dto) false) { result.addError(第 (i 1) 行: validateMessage(dto)); continue; } Employee emp convertToEntity(dto); employeeMapper.insert(emp); successCount; } result.setSuccessCount(successCount); return result; }逻辑说明rollbackFor Exception.class表示任何异常都触发回滚。这里有个容易被忽略的点RuntimeException默认回滚但Exception默认不回滚如果把 Service 层异常包装成了受检异常不加rollbackFor会导致数据半插入状态。我见过太多项目在这里踩坑数据库里多了半截脏数据排查时又看不出哪条是新的。Excel 导入的另一个坑是日期格式。POI 读 Excel 日期时拿到的是数值型序列比如44623.0必须显式转换。ExcelUtil.parse里我封装了HSSFDateUtil判断单元格类型是数值就先转 Date 再格式化成yyyy-MM-dd否则员工生日和入职日期全是 1970 年的值这个 bug 在验收时非常尴尬。5. SSM 项目常见问题与避坑指南部署、版本、编码的实战记录5.1 Maven 依赖冲突导致启动失败spring-web 和 spring-webmvc 版本不一致现象Tomcat 启动时直接抛出NoSuchMethodError或者ClassNotFoundException指向某个 Spring 类但代码里明明有这个类。原因项目里同时存在多个 Spring 版本。常见是传递依赖引入了一个老的 spring-web和 pom 里显式声明的 spring-webmvc 版本打架类加载器加载到了旧版本的类。解决在 pom 里显式声明所有 Spring 制品版本用dependencyManagement统一版本号然后跑mvn dependency:tree看依赖树找到隐藏的旧 spring 包用exclusion排除。排查完成后把整个target目录删掉重新打包防止本地残留的旧 class 干扰编译。5.2 JSP 页面中文乱码过滤器配置顺序不对现象JSP 页面上的中文显示正常但从表单提交到数据库的中文全是问号。原因Spring 自带的CharacterEncodingFilter只处理 POST 请求的编码如果过滤器没有配置forceEncodingtrue或者过滤器在 SpringMVC 的 DispatcherServlet 之后注册请求参数在进入业务代码前已经被按 ISO-8859-1 解析过了。解决在web.xml里把CharacterEncodingFilter放在所有过滤器第一位强制编码filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping同时检查数据库连接 URL 里的characterEncodingutf8和 MySQL 表本身的字符集三个地方有一处断了都会乱码。这套排查顺序我写成了一段顺口溜先看过滤器再看连接串最后看表结构。5.3 MyBatis 的 Mapper XML 文件找不到构建配置遗漏资源目录现象运行时抛org.apache.ibatis.binding.BindingException: Invalid bound statement但 Mapper 接口和 XML 文件的 namespace 明明写对了。原因Maven 工程在打包时默认只把src/main/java下的.java文件编译进 class 目录mapper/*.xml放在src/main/java下面会被直接忽略。如果 XML 文件不在src/main/resources目录打包后 class 目录里就没有对应的 XML 文件。解决一种是把 XML 文件全部放到src/main/resources/mapper/目录另一种是在 pom 里显式配置资源扫描build resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource resource directorysrc/main/resources/directory /resource /resources /build这段配置加完后重新执行mvn clean package -DskipTests再检查target/classes下有没有 XML 文件这个方法可以确认问题确实解决了而不是靠感觉。5.4 Session 失效导致的重复登录前后端分离接口的会话管理现象用户登录成功后操作几分钟又自动跳回登录页特别是在点击「查看工资条」这类通过 AJAX 请求的页面时频繁出现。原因Tomcat 默认 Session 超时时间是 30 分钟但如果是前后端分离部署前端静态页面在 Nginx后端接口在 Tomcat前端请求接口时没有携带 CookieTomcat 每次都会新建 Session登录状态自然存不住。解决检查 Nginx 配置里有没有proxy_set_header Cookie $http_cookie;以及前端 AJAX 请求有没有开xhrFields: { withCredentials: true }。如果是同域部署这个问题不会出现跨域部署时还要配置 CORS 允许携带凭证。对于纯 SSM 的 JSP 项目同域名下一般不会遇到但如果你把前端项目单独部署到 Nginx 了这个坑几乎必踩。5.5 连接池耗尽Druid 的默认参数在大查询下翻车现象系统运行一两天后页面突然报Cannot get a connection, pool exhausted重启才能恢复。原因Druid 默认maxActive8而系统里某个列表页每次查询要打开多个连接比如循环查询部门下的员工。并发几个人同时点同样的页面连接池瞬间被耗尽且连接释放不及时持续堆积后不可用。解决给 Druid 设置合理的连接池参数并在监控页查看 SQL 执行耗时。maxActive根据业务并发设到 20-50minIdle保持 5testWhileIdle设置为 truevalidationQuery用SELECT 1。同时把 Service 层里循环单条查询的代码改成批量查询或一条JOIN查完从源头减少连接占用——连接池参数是治标SQL 优化是治本。6. 上线前必做的验证手段从接口自测到压测脚本把项目从「能跑」提升到「敢上线」我认为有三件事靠经验也必须做接口参数校验的补齐、慢 SQL 的排查、还有一轮简单的并发测试。第一件事检查所有RequestParam参数有没有加required和默认值。SSM 项目的接口参数一旦漏传默认抛 400 错误但前端拿到错误提示是「Bad Request」用户根本不知道哪里填错了。我在项目接手时统一改成了RequestParam(required false)配合手动校验每个接口都返回统一格式的 JSON前端拿到code: 40001这类业务错误码能直接定位到具体字段。这一步做完前后端联调阶段的沟通成本会大幅下降。第二件事用 Druid 的监控页查看慢 SQL。打开/druid/sql.html按执行时间排序看哪些 SQL 超过了 500 毫秒。人力资源系统数据量不大慢 SQL 基本都出在缺少索引的三表 JOIN 上。我最常加的三个索引员工表的dept_id、考勤表的(emp_id, work_date)联合索引、薪资表的(emp_id, salary_month)联合索引。加上之后原来 1 秒以上的月结查询降到几十毫秒。第三件事也是我血泪经验最多的地方——并发插入问题。考勤汇总定时任务运行时如果人事手动在页面上操作批量导入两边同时执行会导致数据不一致。我后来在汇总任务开启时加了一个 Redis 锁用setIfAbsent实现分布式锁设置超时时间为 5 分钟任务结束后释放锁。这一步在 SSM 项目里不算复杂成本很低但效果立竿见影至少能保证定时任务跑完前没有别的写入线程进来干扰。压测环节如果你没有专业的压测工具直接用 Jmeter 或者 Postman 的 collection runner 就能做基础验证模拟 50 人同时登录、20 人同时查询工资条观察接口响应时间的中位数和错误率。不需要追求高并发中小企业内部系统 100 并发已经是天花板重点观察两个指标CPU 有没有打满、数据库连接有没有泄漏。如果一个接口的响应时间随着并发升高呈线性上升多半是 N1 查询问题优先回去查 SQL 而不是加服务器。最后说一个我自己的习惯每次上线前我会在凌晨定时任务的日志里特意搜ERROR关键字确保前一天没有未处理的异常。这个习惯救了我很多次比如某次人事导入员工数据时格式不合法异常被吞掉只留了一条日志第二天财务按错误数据发了工资条再改就很尴尬。人力资源系统的数据直接关系到员工的钱宁可多花半小时查日志也不要让错误数据活过午夜。希望这篇笔记能帮你把这个系统做扎实少踩几个我已经替你踩过的坑。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑