ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+SSM人力资源管理系统落地复盘:从源码到实战的踩坑指南

SpringBoot+SSM人力资源管理系统落地复盘:从源码到实战的踩坑指南 前段时间帮一家做电商代运营的公司把人事管理流程从Excel和微信聊天记录里搬到了系统上前后折腾了一个多月最终落地的就是一套JavaSpringBootSSM人力资源管理系统。这个项目原本只是网上常见的那种课程设计级别的源码包自带一份论文、调试文档和操作讲解看起来什么都有但真正拿到公司环境里去跑问题比想象中多得多。这篇博文就当一次完整复盘从需求拆解、技术选型、数据库设计到核心功能实现、上线前后踩过的坑一条线讲清楚。很多同学拿到这类系统源码后第一反应是“解压、配置、跑起来”但跑起来离“用起来”还有很长的距离。我这次项目的经历就是典型例子源码能跑通业务却对不上界面能打开权限却漏了考勤数据能导入日期却差了8小时。这些坑如果不处理好系统上线等于给自己挖坑。这篇文章里我会把排查思路和最终方案都写出来既适合正在做相关毕设的学生参考也适合想快速落地一套内部人事系统的小团队照抄作业。1. 这个项目要解决的是HR部门每天最头疼的几件事1.1 靠Excel和微信维持人事流程撑不过100人规模这家公司的情况很有代表性。50多人时人事行政两个人还能靠Excel表格、微信群和共享网盘撑住日常运转。可一旦团队规模往80人、100人走问题就像滚雪球一样滚出来了。最典型的场景是员工信息不一致。人事经理手里有一份Excel花名册财务那边有一份工资台账部门主管自己还记着一份团队名单。员工转正、调岗、离职之后三份数据不一定同步更新结果月底核算工资时财务按旧数据算人事按新数据改来回扯皮。考勤更麻烦。公司用的是传统考勤机月底导出一份打卡记录Excel人事再手工核对迟到、早退、漏卡、请假、加班一个人对着几千行数据看两三天。中间任何一个假别、补卡申请躺在聊天记录里没被统计进去就会造成工资算错员工投诉。这些都是真实发生过的场景不是编出来的。我的判断是这个阶段最需要的不一定是功能多么花哨的HR系统而是把“人、考勤、薪酬、权限”这四件事管起来让所有数据有个统一的、能追溯的出处。1.2 该做哪些功能模块边界的取舍拿到源码后我发现它内置的模块其实做了很多包括员工管理、部门管理、考勤管理、请假管理、薪酬管理、招聘管理、培训管理、系统管理这些。但实际落地时我并没有把每个模块都推给业务方用而是做了很明确的功能裁剪。最终上线的是七个必用模块员工管理员工花名册、入职、转正、调岗、离职全流程部门管理部门树结构、部门负责人设置考勤管理考勤机数据导入、打卡记录查询、异常标记请假管理请假申请与审批自动联动考勤和薪酬薪酬管理月度工资核算、工资条查看招聘管理简历登记、面试安排、录用登记系统管理用户、角色、菜单权限分配培训管理这种模块公司短期内用不上先放在后台不开放入口。经验是给业务方上系统时按钮越少越不容易出乱子。HR系统这种东西用户体验不是第一位的数据准确和流程闭环才是第一位的。1.3 从启动到上线的时间规划整个落地周期是五周大致分配是这样的第一周需求调研、模块边界确认、数据库调整第二周核心技术栈改造、权限模块重写第三周员工、考勤、请假三个核心模块联调第四周薪酬模块配置、历史数据迁移第五周试运行、培训、正式切换前面两周最关键因为源码里用的还是老一套SSM框架结构Spring的XML配置一大堆我需要把它改造到SpringBoot自动配置模式下但又不想推翻原有的业务代码。这个“改造”的思路直接决定了后面的技术选型。2. 技术选型复盘SpringBootSSM这套组合为什么合适2.1 对比纯SSM与纯SpringBoot中间态的务实之处这套系统的原始技术栈写的是SpringBootSSM网上搜一下也会发现大量同类项目都叫这个名字。其实它描述的是一种“SpringBoot工程结构 SSM框架组合”的形态SpringBoot负责自动配置和启动Spring MVC负责Web层Spring负责事务管理MyBatis负责数据持久化。我做了个对比方便理解它们之间的差异方案优点缺点适用场景传统SSMXML配置框架边界清晰教学友好配置文件太多环境搭建繁琐教学、老项目维护纯SpringBootJPA开发效率高搬代码少复杂查询、动态SQL不顺手快速CRUD项目SpringBootSSM自动配置灵活SQL兼顾需要理解两套体系的叠加逻辑中小型管理系统、课程设计改造对我这种“要快速交付给公司用”的情况来说SpringBootSSM是性价比很高的方案。SpringBoot的自动配置省去了大量XML配置内嵌Tomcat也让部署变得简单——一个java -jar命令就能起服务。MyBatis则保留了复杂查询的控制力HR系统里有大量的动态查询条件比如员工列表需要按部门、姓名、入职时间、状态组合筛选用MyBatis写动态SQL非常顺手。2.2 MyBatis在人事场景中的优势员工多条件检索、薪酬历史对比、考勤异常统计这些查询都是典型的多条件动态组合。用JPA当然也能写但规格化查询Specification写起来既绕又不好调试远不如MyBatis的if标签一眼看明白。这里给一段我改造后的员工分页查询SQL用的是PageHelper分页插件select idselectEmployeePage resultTypecom.hr.system.entity.Employee SELECT e.id, e.emp_no, e.name, e.gender, e.dept_id, e.position, e.phone, e.entry_date, e.status, e.id_card FROM hr_employee e where if testempNo ! null and empNo ! AND e.emp_no LIKE CONCAT(%, #{empNo}, %) /if if testname ! null and name ! AND e.name LIKE CONCAT(%, #{name}, %) /if if testdeptId ! null AND e.dept_id #{deptId} /if if teststatus ! null AND e.status #{status} /if /where ORDER BY e.entry_date DESC /selectPageHelper的用法注意一点在SpringBoot里引入pagehelper-spring-boot-starter之后只要在Mapper查询方法前调用PageHelper.startPage(pageNum, pageSize)紧接着的第一条SQL就会被自动加上分页限制。这个“紧接着”很关键中间不要执行其他查询否则分页会加错SQL上。2.3 技术版本与项目结构我最终使用的版本清单如下都是踩过坑之后固定下来的JDK 1.8SpringBoot 2.7.18MyBatis 3.5.x mybatis-spring-boot-starter 2.3.2MySQL 8.0.33Druid连接池 1.2.20Redis 2.7.3用于验证码和部门缓存PageHelper 1.4.7Apache Shiro 1.13权限框架这里特别提示一下如果你的电脑装的是JDK 1.8SpringBoot别选3.x版本。SpringBoot 3.x要求JDK 17起步很多同学遇到“IDEA创建SpringBoot项目不能用JDK 1.8”的问题就是因为版本配对没搞对。老老实实选择2.7.x这是目前对JDK 1.8最友好的线。项目结构上我做了一个调整把原本扁平的单模块改成了Maven多模块拆分让业务边界更清晰hr-system/ ├── hr-common // 通用工具类、统一返回结果、异常处理 ├── hr-framework // 配置类、安全框架、拦截器 ├── hr-system // 系统管理模块用户、角色、菜单 ├── hr-admin // 后台管理Web入口不过说实话对于100人规模的公司内部系统多模块拆分并不是必须的。如果项目本身就是单模块完全可以直接在原来的结构上改。我拆模块是因为后期计划扩展移动端接口把Web入口和业务核心分开可以避免以后大改。2.4 pom依赖里最容易配错的细节给一个精简版的pom.xml核心依赖片段这是可以直接用的parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies !-- Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency !-- MySQL驱动注意8.0的driver-class-name变了 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- Druid连接池 -- dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.20/version /dependency !-- 分页插件 -- dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency /dependencies注意MySQL 8.0之后驱动类名必须写成com.mysql.cj.jdbc.Driver不是老版本的com.mysql.jdbc.Driver。另外连接串务必带上useSSLfalse和serverTimezoneAsia/Shanghai后面讲时区坑的时候你就能体会到这一行的分量。3. 数据库设计先把人事数据的“地基”打牢3.1 六张核心表搞清楚业务关系HR系统的数据模型不复杂但表之间的关系一定要理清。我最终保留了六张最核心的业务表关系是这样的sys_user系统用户对应的是能登录系统的人可以是HR、部门主管、员工本人sys_role角色和sys_user_role用户角色关联构成RBAC权限模型的基础hr_department部门表用父子关系维护树形结构hr_employee员工表是业务核心与部门、用户关联hr_attendance考勤表一条记录对应某员工某一天的考勤情况hr_salary薪酬表对应用户在某薪酬周期内的核算结果再加上hr_leave、hr_recruit这些辅助表一个完整的数据闭环就成了。可以这么理解用户和角色管“谁能登录、能看什么”部门和员工管“公司有哪些人”考勤和薪酬管“这些人怎么算钱”三个圈互有交叉但职责清晰。3.2 员工档案表字段细节与DDL示例员工表是整个系统里最需要细心设计的因为客户信息错了很难追。以下是我调整后的建表SQL注释里写清了每个字段的用途CREATE TABLE hr_employee ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键, emp_no varchar(32) NOT NULL COMMENT 工号唯一, name varchar(64) NOT NULL COMMENT 姓名, gender tinyint(4) DEFAULT NULL COMMENT 性别1男 2女 0未知, dept_id bigint(20) NOT NULL COMMENT 部门ID, position varchar(128) DEFAULT NULL COMMENT 岗位名称, phone varchar(20) DEFAULT NULL COMMENT 手机号, id_card varchar(64) DEFAULT NULL COMMENT 身份证号加密存储, entry_date date DEFAULT NULL COMMENT 入职日期, regular_date date DEFAULT NULL COMMENT 转正日期, leave_date date DEFAULT NULL COMMENT 离职日期, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 员工状态1在职 2试用 3离职, create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, deleted tinyint(1) NOT NULL DEFAULT 0 COMMENT 逻辑删除标记, version int(11) NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, PRIMARY KEY (id), UNIQUE KEY uk_emp_no (emp_no), KEY idx_dept_id (dept_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT员工信息表;几个容易忽略的点emp_no必须唯一工号一旦生成不回收即使员工离职历史工号也要保留不能发给下一个人否则历史考勤和薪酬记录会串人。id_card存的是加密值不是明文。HR系统里的身份证号是最敏感的数据后面会专门说脱敏方案。deleted字段做逻辑删除员工离职不等于删除记录只是标记状态变更。version字段是为乐观锁准备的防止两个人同时编辑同一员工的信息导致覆盖。3.3 考勤和薪酬表的字段策略考勤表的设计要点是“一条记录对应一个人一天”。这样可以很方便地按月汇总也能精确到某一天。CREATE TABLE hr_attendance ( id bigint(20) NOT NULL AUTO_INCREMENT, emp_no varchar(32) NOT NULL COMMENT 工号, attendance_date date NOT NULL COMMENT 考勤日期, check_in_time datetime DEFAULT NULL COMMENT 上班打卡时间, check_out_time datetime DEFAULT NULL COMMENT 下班打卡时间, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 考勤状态1正常 2迟到 3早退 4漏卡 5请假 6出差 7加班, remark varchar(255) DEFAULT NULL COMMENT 备注, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_emp_date (emp_no, attendance_date), KEY idx_attendance_date (attendance_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考勤记录表;这里uk_emp_date唯一索引特别重要它从数据库层面保证了同一员工同一天只能有一条考勤记录避免重复导入把数据搞乱。薪酬表的设计则需要考虑“快照”概念。员工的工资不是一成不变的调薪之后历史数据不能被覆盖。所以薪酬记录必须是每次核算生成一个新的快照包含当月的基本工资、绩效、补贴、扣款等所有明细而不是关联员工基础表去实时计算。3.4 逻辑删除、唯一索引与数据纠错在实际运维中逻辑删除和唯一索引是有冲突的。比如员工表里emp_no是唯一的某员工离职后逻辑删除但如果过几个月又来了一模一样的工号要重新入职唯一索引就会挡住。解决思路其实很简单工号本来就该保留不再复用新员工给新工号不需要回收。还有一个坑是批量导入数据时如果Excel里的数据没有做去重重复导入会导致唯一索引冲突。我的做法是导入逻辑分两步先校验再插入。校验阶段查出所有与库中重复的工号批量返回给用户一次性提示错误不要插一条错一条。4. 核心功能模块实现从登录到薪酬的链路4.1 基于RBAC的权限控制权限这块我直接用原项目里的Apache Shiro框架没有换Spring Security理由是业务方要求的权限粒度还没复杂到必须上Spring Security。RBAC模型在中小型管理系统里完全够用用户-角色-菜单。核心权限控制方式有两种URL级控制和按钮级控制。URL级控制用Shiro的shiroFilterFactoryBean配置比如/salary/**只允许拥有salary:list权限的角色访问。按钮级控制则是通过自定义注解拦截器实现比如页面上的“删除”按钮通过RequiresPermissions(employee:delete)来控制显示和提交。我把权限表设计的比较简单五张表sys_user、sys_role、sys_menu、sys_user_role、sys_role_menu。菜单表同时承担“菜单展示”和“权限标识”两个功能通过perms字段来标识权限点。4.2 员工管理接口的设计员工管理是HR每天用得最多的功能。我保留了核心的增删改查接口同时增加了一些业务校验逻辑。举个例子离职操作不是简单把状态改成3而是要经历一个完整流程校验该员工是否存在未审批的请假单校验该员工是否还有未结清的借款或报销如果有提示先处理根据离职日期计算该月工资结算金额将员工状态改为“离职”记录离职日期将该员工的系统账户禁用这些规则看起来琐碎但都是真实业务里必须考虑的。如果离职员工的账户还留着他还能登录系统查看全公司薪资那问题就大了。Controller层接口设计我遵循了REST风格统一返回Result对象RestController RequestMapping(/api/employee) public class EmployeeController { Autowired private EmployeeService employeeService; GetMapping(/page) public Result page(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, EmployeeQuery query) { PageResultEmployeeVO page employeeService.queryPage(pageNum, pageSize, query); return Result.success(page); } PostMapping public Result add(RequestBody EmployeeDTO dto) { employeeService.addEmployee(dto); return Result.success(); } PutMapping(/{id}) public Result update(PathVariable Long id, RequestBody EmployeeDTO dto) { employeeService.updateEmployee(id, dto); return Result.success(); } PostMapping(/{id}/leave) public Result leave(PathVariable Long id, RequestBody LeaveInfoDTO dto) { employeeService.handleLeave(id, dto); return Result.success(); } }建议所有写操作不要直接返回成功而是返回受影响的数据或至少返回成功标识这样前端可以做二次确认。4.3 考勤处理打卡导入、规则配置、异常修正这家公司没有上人脸识别考勤机用的还是传统IC卡考勤机。所以系统里做了考勤机数据导入功能支持Excel文件批量导入原始打卡记录。导入后的处理流程分三步清洗原始数据去掉重复打卡、识别无效设备号、过滤非本公司工号按“员工日期”维度聚合从多次打卡记录中提取第一次作为上班打卡时间最后一次作为下班打卡时间套用考勤规则判断状态比如上午9点前打卡算正常9点到9点半算迟到超过9点半算旷工半天下午18点30分前未打卡算早退考勤规则做成配置化在系统管理里可以调整而不是写死在代码里。上下班时间、迟到容忍分钟数、加班开始时间、请假抵扣规则都放进配置表。这样公司调整作息时间业务人员自己就能改不用改代码重新发布。遇到漏打卡的情况系统支持在线补卡申请。员工发起补卡申请部门主管审批通过后自动把考勤记录的状态从“漏卡”改为“正常”。这些细节在源码里其实没有都是我在联调阶段补上的。建议有类似需求的项目重点检查这部分的逻辑闭环。4.4 薪酬核算公式、快照与批量生成薪酬模块是最容易出错的地方也是员工最关注的地方。我对照公司提供的工资计算规则把算法重写了一遍public BigDecimal calculateSalary(SalaryContext context) { // 1. 应发合计 基本工资 岗位工资 绩效 餐补 加班工资 BigDecimal baseSalary context.getBaseSalary(); BigDecimal postSalary context.getPostSalary(); BigDecimal performance context.getPerformance(); BigDecimal mealAllowance context.getMealAllowance(); BigDecimal overtimePay context.getOvertimePay(); BigDecimal grossPay baseSalary.add(postSalary) .add(performance).add(mealAllowance).add(overtimePay); // 2. 缺勤扣款 BigDecimal absenceDeduct context.getAbsenceHours() .multiply(context.getHourlyRate()); // 3. 五险一金个人部分 个税 BigDecimal socialSecurity context.getSocialSecurity(); BigDecimal housingFund context.getHousingFund(); BigDecimal tax TaxCalculator.calculate(grossPay.subtract(socialSecurity) .subtract(housingFund).subtract(5000).max(BigDecimal.ZERO)); // 4. 实发工资 return grossPay.subtract(absenceDeduct) .subtract(socialSecurity).subtract(housingFund).subtract(tax); }每月薪酬核算的操作路径是选择核算月份系统自动从员工表和考勤表汇总数据生成该月薪酬列表HR逐条核对后点击“确认发放”此时数据快照写入薪酬历史表员工就可以在系统里查看自己的工资条。这个“确认”动作很关键一旦确认就有历史快照之后调整不会影响已经发放的数据。工资条展示时要注意基本工资、绩效、五险一金这些字段员工本人只能看自己的部门主管只能看本部门员工的“应发合计”“实发合计”不能看具体明细只有HR和财务角色能看全量明细。这种差异化展示靠的就是前面权限模块的数据过滤规则。5. 上线前后踩过的坑完整排查过程复盘5.1 考勤日期偏移8小时时区问题排查链路系统上线后的第二天HR就反馈了一个诡异问题凌晨打卡的记录日期全都记到了前一天晚上。比如有员工凌晨1点下班打卡系统显示的日期是昨天的日期。我的排查链路是这样的第一步看数据库里的原始数据。执行SELECT * FROM hr_attendance ORDER BY check_in_time DESC LIMIT 20发现数据库里存的时间确实已经是前一天说明问题不在前端展示而是数据写入时就错了。第二步看应用日志打印出接收到的打卡时间点参数发现接口收到的参数年份、月份、日期都正确问题出在持久化环节。第三步看JDBC连接串Druid连接池配置里的serverTimezone写着UTC。UTC比北京时间慢8小时当Java代码里的LocalDateTime值经过JDBC转换成数据库的DATETIME时MySQL会根据连接时区的设定做转换导致时间往回拨了8小时。修复方案很简单把连接串统一改成url: jdbc:mysql://localhost:3306/hr_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue同时JVM时区也做了约定项目启动参数加上-Duser.timezoneAsia/Shanghai保证容器时区、JVM时区、数据库连接时区三者一致。5.2 Excel批量导入把内存打满从POI到EasyExcel这个坑出现在测试环境HR导入了3000条员工历史数据应用直接OutOfMemoryError。根本原因很明确Apache POI的XSSFWorkbook在处理.xlsx文件时是把整个文件以DOM方式加载进内存的3000条数据看似不多但每行几十个字段对象模型撑起来就是几万个节点内存直接被打爆。排查过程用了两个工具先看异常日志确认是java.lang.OutOfMemoryError: Java heap space再用jstat -gcutil pid观察GC曲线发现老年代内存持续增长无法回收基本锁定了大对象常驻内存的问题。修复方案是把导入组件从POI的XSSFWorkbook换成阿里开源的EasyExcel它的方式是一行一行地从文件读取并解析内存占用极低。改造之后的代码结构大概是// EasyExcel流式读取逐行处理 EasyExcel.read(file.getInputStream()) .head(EmployeeImportDTO.class) .registerReadListener(new EmployeeImportListener(employeeService)) .sheet() .doRead();我还额外做了两层保护一是在接口层限制单次导入不超过5000行二是在Service层做分批提交每500条进行一次flush和commit防止单事务积累过多未提交数据。导入完成之后返回一个统计结果包含成功条数和每行失败的原因方便HR对照Excel修正。5.3 方法内的“自己调用自己”导致事务失效这个坑是在批量导入时发现的当第100条数据校验失败时前99条居然已经写进了数据库。业务预期是一次导入就是一个整体要么全部成功要么全部回滚。我一眼扫过代码Service方法上的Transactional注解明明存在为什么没生效排查链路如下第一步确认事务管理器是否配置正确。检查配置发现用的是SpringBoot默认的DataSourceTransactionManager配置层面没问题。第二步看方法修饰符。Transactional只对public方法生效确认方法本身是public没问题。第三步看异常是否被吞掉。对代码里的try-catch做了检查发现异常确实被捕获并且重新抛出了排除吞异常的情况。第四步查到根源了同类内部方法调用导致Transactional失效。代码是一个隐患很深的写法Service public class EmployeeServiceImpl implements EmployeeService { Transactional(rollbackFor Exception.class) public void batchImport(ListEmployeeDTO list) { for (EmployeeDTO dto : list) { // 循环调用同类的保存方法 this.saveSingle(dto); } } Transactional(rollbackFor Exception.class) public void saveSingle(EmployeeDTO dto) { employeeMapper.insert(dto); } }Java里this.saveSingle()走的是普通方法调用没有经过Spring的代理对象所以事务注解完全不解析。这里有个实际用过的修复方案注入自身的代理对象来替代this调用具体方式是在类中加上Autowired或Resource注入EmployeeService然后在批量方法里调用self.saveSingle(dto)这样就能触发代理逻辑。不过最优雅的方式还是把批量保存逻辑单独拆分到一个事务边界类里从语义上隔离“批量校验”和“逐条保存”这两个层次。5.4 薪资数据“看得见但看不到”权限只做了URL层上线一周后有部门主管反馈自己明明有“薪资管理”菜单也能打开薪资列表页面但列表数据是空的而其他部门的主管能看到数据。这个现象很诡异权限要么有要么没有为什么会出现“部分可见”排查发现原来的权限控制只做了URL级别的拦截。也就是说过滤器检查了你有没有访问/salary/list这个URL的权限但没有校验这个URL返回的数据是不是你该看的。所以部门主管A访问薪资列表后端返回的是全公司所有部门的薪资数据权限拦截器只负责放行请求但SQL查询本身没有按部门过滤。真正的根因是“功能权限”和“数据权限”是两个维度。URL拦截管的是“你有没有资格用这个功能”数据过滤管的是“这个功能返回的数据你有没有资格看”。原来的源码只做了前者后者完全缺失。我在这块做了一个专门的数据权限切面处理核心思路是在薪资查询Mapper执行前根据当前登录用户所属部门自动拼上部门过滤条件。部门主管只看本部门HR和财务看全公司。此时自定义注解的作用就很明显了用DataScope(deptAlias e, userAlias e)标注在查询方法上切面解析注解并动态修改SQL这样业务代码里不需要到处传部门ID。6. 性能优化与后续演进方向6.1 不追求高并发但要把大表查询优化好HR系统的访问量其实很小一家100人的公司峰值并发可能也就十几个人同时操作。性能瓶颈不在并发量而在历史数据的堆积和复杂查询的效率。考勤表一年就能积累3万多条记录如果连续用三年就是10万级别。薪酬表同理每个员工每月一条三年也有几千条。优化思路按优先级排序建立合适的联合索引。根据查询频率最高的条件设计索引考勤表按(emp_no, attendance_date)建唯一索引薪资表按(emp_no, salary_month)建唯一索引。大字段和主表拆开。员工表的“籍贯”“紧急联系人”“家庭住址”这些低频字段建议拆到hr_employee_ext扩展表主表只保留高频查询字段这样单行数据更小索引扫描效率更高。冷热数据分离。超过两年的考勤数据可以归档到历史表主表只保留近期数据查询响应速度会明显提升。缓存字典表。部门名称、岗位名称这些几乎不变化的字典数据加载到Redis或者本地内存缓存中避免每次列表查询都去关联查表。6.2 敏感字段脱敏与操作日志HR系统的数据敏感性极高身份证号、手机号、薪资信息都属于敏感数据。上线前我把展示层做了脱敏处理身份证号只显示前3位和后4位手机号只显示前3位和后4位薪资明细只对特定角色可见。脱敏在代码层面实现了两种方式一种是在返回VO对象时通过Jackson的JsonSerialize注解自定义序列化器对敏感字段做脱敏处理另一种是在查询接口里直接使用SQL函数截取字段比如CONCAT(LEFT(id_card, 3), **********, RIGHT(id_card, 4))。前者更灵活后者性能更好看业务取舍。另外给所有涉及敏感字段的查询、修改、导出操作都加上了操作日志记录操作人、操作时间、操作参数。这是审计需要也是事后追责的依据。源码里这个功能比较薄弱我花了半天补完建议类似项目上线前都检查一下这个能力。6.3 从人事管理延伸到OA协同系统跑通之后我发现一个很自然的扩展方向把人相关的流程和行政、日常办公流程打通就变成轻量级OA系统。比如请假审批通过后自动同步到考勤系统员工入职后自动创建企业邮箱账号、企业微信群薪酬确认后自动推送工资条到员工端。如果之后需要移动端能力可以优先考虑把后端接口REST化做成独立API服务前端小程序或者App单独开发。这样SpringBootSSM的系统不需要推翻重来它作为核心业务服务继续运行只是把接口暴露出来给新的客户端使用。我个人的体会是做系统集成项目最忌讳一上来就推倒重来。源码里哪怕有一半代码能用也比从零开始快得多。但前提是你能分清哪些能用、哪些必须改。比如权限模块和数据模型属于系统的“地基”不牢固就直接换像员工列表、导出报表这种页面能用就先留着等核心跑通再慢慢优化。最后再分享一个当时帮了大忙的小技巧在改造之前先把项目完整跑起来逐个模块截图记录原始功能再对照实际业务梳理差异清单。这个清单就是后面排期的施工图也是跟HR部门沟通需求时的对齐工具。很多人拿到一套源码上来就改代码改到一半发现业务对不上又回头改需求一来一回浪费的时间比写代码多得多。项目落地靠的不是会写代码而是能把“代码世界”和“业务世界”无缝对应起来。
返回列表