简介:本资源是一套完整的人力资源管理信息系统(HRMIS)Java Web源码实现,面向Java后端开发初学者与企业信息化系统学习者,助力理解HR核心模块(员工、招聘、绩效、薪酬、考勤、培训)的工程化落地。压缩包含1272个文件,总大小20.85MB,主体为405个htm/5个html页面构成前端展示层,433个gif与33个png支撑界面交互,67个js及38个jsp实现动态逻辑,34个java与34个class文件构成Spring+JSP架构下的业务与控制层,另有35个jar包、27个xml配置及1个sql脚本提供环境与数据支持。内容预览显示treeData.ashx、EmployeeAction.class、ClockDAO.class等关键组件,体现典型分层结构与权限驱动设计。目前已有987人学习下载,读者可直接部署运行、分析MVC调用链、复用DAO与Action模板、参考考勤与薪资计算等业务逻辑实现,是掌握传统Web HR系统开发流程的优质实践样本。
1. 人力资源管理信息系统源码:不是拿来就能跑的“开箱即用”,而是要亲手拧紧每颗螺丝的业务底盘
很多人搜到“人力资源管理信息系统源码”第一反应是——下载、解压、启动,然后坐等组织架构图自动铺开、考勤数据实时跳动、薪酬报表一键生成。现实却是:90% 的源码包解压后连登录页都刷不出来,剩下10% 能进系统,但员工档案一改就报错,审批流配置半天不触发,离职流程卡在“待归还资产”环节死循环。这不是代码质量差,而是这类系统天然带着三重硬约束:业务逻辑强耦合、组织规则高度定制、数据权限粒度极细。它不像博客系统或商城模板,可以靠通用组件堆出来;它必须和你公司真实的岗位体系、职级带宽、假勤政策、社保缴纳地规则严丝合缝咬合。所以本文不讲“如何安装XX系统”,而是带你从源码根目录开始,逐层拆解一个可落地的HRMIS源码项目该怎样被真正“唤醒”——验证数据库字段是否匹配本地社保基数表、重写审批引擎的条件判断链、把Excel导入模板里的“部门编码”映射成你内部OA的真实树形ID。适合正在接手二次开发、准备做国产化迁移、或想用开源底座搭私有HR平台的工程师。别怕源码里满屏的EmployeeService.java和AttendanceRuleEngine.groovy,我们只盯住三件事:数据通不通、流程走不走、权限卡不卡。
2. 源码结构解剖与核心模块定位:先认准哪几块代码决定系统生死
拿到一个HRMIS源码包(常见命名如hrmis-springboot、hr-platform-core或hrms-source-v3.2),别急着mvn clean install。第一步是像外科医生划刀口一样,精准切开目录,找到真正影响业务运转的“命脉模块”。我经手过的17个主流HR源码项目(含某高校实验室维护的开源框架、某公司内部迭代8年的私有版本),其结构虽有差异,但核心骨架高度一致。下面这张表不是罗列所有文件夹,而是标出你必须优先打开、逐行读、重点调试的5个目录:
| 目录路径(示例) | 关键文件类型 | 为什么必须盯死它 | 典型风险点 |
|---|---|---|---|
/src/main/java/com/hr/core/employee/ | EmployeeEntity.java,EmployeeMapper.xml | 员工主数据是所有模块的基石。字段缺失(如缺probation_end_date)、类型错配(entry_date存为String)、注解冲突(@Table(name="t_emp")但实际库表叫hr_employee)会导致入职、转正、离职全流程崩坏 | 字段名与你现有HR数据库不一致;枚举类EmployeeStatus值(如"ONBOARDING")和你公司定义的"试用中"不对应 |
/src/main/resources/config/ | application-prod.yml,sharding-jdbc.yml | 生产环境数据库连接、分库分表策略、Redis缓存开关全在这里。很多源码默认用H2内存库或localhost:3306,直接启动必然连不上你的MySQL集群 | spring.datasource.url未替换;sharding.jdbc.config.sharding.tables.t_attendance.actual-data-nodes指向不存在的分片库 |
/src/main/java/com/hr/workflow/ | ApprovalProcessDefinition.java,WorkflowEngine.java | 审批流不是前端按钮,而是由DSL或Java规则驱动的状态机。LeaveApplicationProcess里写的if (days > 3) goto 'dept_manager' else goto 'hr_bp',若你公司规定“年假超5天才需总监审批”,这里就是雷区 | 条件表达式硬编码;节点审批人角色(如ROLE_HRBP)未对接你公司的RBAC权限体系 |
/src/main/java/com/hr/salary/ | SalaryCalculator.java,TaxDeductionRule.groovy | 薪酬计算是黑匣子中的黑匣子。TaxDeductionRule.groovy这种脚本式规则,一行baseSalary * 0.12可能忽略你所在城市公积金缴存比例上限(如深圳最高12%,但东莞是5%-12%浮动) | 税率表未更新(2023年个税起征点仍是5000,但专项附加扣除标准已调);社保基数上下限写死,未关联social_security_config表 |
/src/main/webapp/static/js/或/src/main/resources/static/js/ | org-tree.js,approval-form.js | 前端不是装饰。org-tree.js里loadChildren(nodeId)请求的API路径若写成/api/v1/dept/children?id=1,而你后端RESTful规范是/api/departments/{id}/sub-departments,整个组织架构树就空白 | 请求URL拼写错误;Vue组件中v-model绑定的字段名(如empForm.positionLevel)与后端DTO的positionLevelCode不一致 |
提示:别被
/test/目录迷惑。很多源码的单元测试只覆盖了UserEntity的getter/setter,对SalaryCalculator.calculate()这种核心方法零覆盖。真正的校验点永远在生产配置和数据库交互层。
2.1 用最小命令验证数据库连通性:绕过Spring Boot自动装配的“捷径”
很多新手卡在第一步:Application.java启动报Connection refused。此时别急着查application.yml,先用最原始的方式确认网络和账号权限是否真通:
# 进入源码根目录,找到数据库配置文件(通常在 resources/config/ 下) grep -r "jdbc:mysql" src/main/resources/config/ --include="*.yml" --include="*.properties" # 输出示例:src/main/resources/config/application-prod.yml:spring.datasource.url=jdbc:mysql://192.168.10.5:3306/hr_prod?useSSL=false&serverTimezone=Asia/Shanghai # 提取IP、端口、库名,用mysql客户端直连(假设账号密码为 hr_app / Hr@2024!) mysql -h 192.168.10.5 -P 3306 -u hr_app -pHr@2024! hr_prod -e "SELECT VERSION();"如果这步失败:
Can't connect to MySQL server→ 检查防火墙(telnet 192.168.10.5 3306)、MySQL是否监听外网(bind-address = 0.0.0.0inmy.cnf)、账号授权(GRANT ALL ON hr_prod.* TO 'hr_app'@'%');Access denied→ 密码错误或账号无库权限;Unknown database 'hr_prod'→ 库不存在,需手动建库并导入初始SQL(见下一节)。
参数说明:
-P(大写P)指定端口,-p(小写p)后紧跟密码(无空格!)。生产环境严禁在命令行明文传密码,此处仅为快速验证。
2.2 初始化数据库:别信“自动建表”,手写SQL才是唯一可靠路径
几乎所有HRMIS源码都声称支持spring.jpa.hibernate.ddl-auto=update自动建表。但血泪经验是:它只适用于全新、无业务数据的玩具库。真实场景下,update会破坏已有索引、误删你自定义的扩展字段、把VARCHAR(255)强行改成TEXT导致查询变慢。正确做法是——用源码自带的sql/目录下的初始化脚本,手工执行。
常见源码包结构:
hrmis-source/ ├── sql/ │ ├── init_schema.sql # 建库、建表、设主键、基础索引 │ ├── init_data.sql # 插入系统管理员、默认角色、基础字典(如性别、民族) │ └── upgrade_v2.1_to_v2.2.sql # 版本升级脚本(本次不用)执行步骤(以MySQL为例):
# 1. 创建空库(字符集必须为utf8mb4,否则中文姓名存乱码) mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS hr_prod CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" # 2. 执行建表脚本(注意路径) mysql -u hr_app -pHr@2024! hr_prod < sql/init_schema.sql # 3. 执行基础数据(关键!缺少sys_admin用户,后台登录直接404) mysql -u hr_app -pHr@2024! hr_prod < sql/init_data.sql逻辑说明:
init_schema.sql里重点关注三类语句:
CREATE TABLE t_employee (...) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;→ 确认引擎是InnoDB(支持事务),字符集是utf8mb4(兼容emoji和生僻字);ALTER TABLE t_employee ADD INDEX idx_dept_id (dept_id);→ 检查高频查询字段(如部门ID、状态)是否有索引;INSERT INTO t_sys_user (username, password, status) VALUES ('admin', '$2a$10$...', 'ACTIVE');→ 记下这个初始密码的加密方式($2a$是BCrypt,需用相同算法校验)。
3. 启动与登录:绕过OAuth2和CAS的“裸奔模式”调试法
源码启动报Invalid redirect_uri或CAS authentication failed?别急着啃OAuth2文档。绝大多数HRMIS源码在开发阶段都预留了“调试开关”,让你绕过复杂的单点登录,直连系统内核。这是工程师的后悔药。
3.1 定位并启用本地登录开关:三步找到那个被注释掉的@Profile("dev")
在Spring Boot项目中,查找以下特征代码:
- 文件路径:
src/main/java/com/hr/config/SecurityConfig.java或WebSecurityConfig.java - 关键注解:
@Profile("!prod")、@ConditionalOnProperty(name="hr.security.debug", havingValue="true") - 核心方法:
configure(HttpSecurity http)内部是否有被//注释掉的http.formLogin()块?
典型代码片段:
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { // 【关键】找这段被注释的代码! /* http.authorizeRequests() .antMatchers("/login", "/error").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .defaultSuccessUrl("/dashboard", true) .failureUrl("/login?error"); */ // 而下面这段是生产用的CAS/OAuth2,先注释掉它 // http.apply(casAuthenticationFilter()); } }操作:
- 取消
/* ... */注释,删除// http.apply(casAuthenticationFilter());这一行; - 在
application-dev.yml中添加开关:
hr: security: debug: true- 启动时指定profile:
java -jar hrmis.jar --spring.profiles.active=dev
参数说明:
--spring.profiles.active=dev告诉Spring加载application-dev.yml,其中hr.security.debug=true会触发@ConditionalOnProperty条件,让formLogin()配置生效。此时访问http://localhost:8080/login,输入init_data.sql里插入的admin账号即可登录。
3.2 登录后首屏白屏?检查前端资源路径与后端API前缀的“隐形契约”
登录成功却看到一片空白?F12看Console报GET http://localhost:8080/api/v1/employees 404?这不是后端没启,而是前后端分离项目里,前端构建产物的API代理配置和后端实际路由不匹配。
定位问题:
- 前端源码(若包含):检查
vue.config.js或webpack.config.js中的devServer.proxy:
// vue.config.js module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', // 后端地址 changeOrigin: true, pathRewrite: { '^/api': '/api' } // 关键!重写规则 } } } }- 后端源码:检查
@RestController的@RequestMapping前缀:
@RestController @RequestMapping("/api/v1") // 注意这个前缀!必须和前端proxy的pathRewrite一致 public class EmployeeController { @GetMapping("/employees") // 实际访问路径是 /api/v1/employees public List<Employee> list() { ... } }修复方案:
若前端proxy写的是'^/api' -> '/api',而后端Controller是@RequestMapping("/v1"),则访问路径变成/api/v1/employees,但后端只认/v1/employees。此时要么改前端proxy:
pathRewrite: { '^/api': '' } // 去掉/api前缀,让请求直送/v1/...要么改后端:
@RequestMapping("/api/v1") // 补上/api前缀记住:前后端的路径契约,比JWT密钥还重要——密钥错了报401,路径错了直接404,连日志都找不到请求痕迹。
4. 避坑:HRMIS源码部署中5个高频翻车现场与解法
注意:以下全是真实踩坑记录,按发生频率排序。每个坑都附带“现象→原因→解决”三段式,拒绝模糊描述。
4.1 现象:考勤打卡页面显示“暂无数据”,但数据库t_attendance表里明明有今天记录
原因:源码中AttendanceService.listToday()方法写了硬编码时区ZonedDateTime.now(ZoneId.of("GMT+8")),而服务器系统时区是UTC,导致now()返回的时间比北京时间晚8小时,查询条件WHERE check_time >= '2024-06-15 00:00:00'实际查的是UTC时间的0点(即北京时间8点),漏掉了早8点前的打卡。
解决:
- 查服务器时区:
timedatectl status | grep "Time zone"; - 统一改为使用Spring Boot的
spring.jackson.time-zone=GMT+8+spring.jackson.date-format=yyyy-MM-dd HH:mm:ss; - 重写查询逻辑,用
LocalDateTime.now()替代ZonedDateTime.now(),并确保数据库字段类型为DATETIME(非TIMESTAMP,后者会自动转时区)。
4.2 现象:薪酬计算结果比Excel手工算的少200元,且每次都不一样
原因:SalaryCalculator.java中调用了TaxDeductionRule.groovy脚本,而该脚本里有一行def base = employee.salary * 0.8——它把“应发工资”当成了“基本工资”,但你公司制度是“基本工资+岗位工资+绩效工资”三者之和才是计税基数。
解决:
- 找到
TaxDeductionRule.groovy,定位计算基数的变量; - 改为从
employee对象中取totalPreTaxSalary字段(若无此字段,需在EmployeeEntity中添加,并在入职/调薪接口中赋值); - 关键验证:在
SalaryCalculator.calculate()方法开头加日志:log.info("Tax base for {} is {}", employee.name, base),重启后看日志输出是否符合预期。
4.3 现象:审批流走到“部门负责人”节点,但系统始终不发送待办通知
原因:源码中NotificationService.sendApprovalNotice()方法调用了emailSender.send(),但emailSenderBean被@ConditionalOnProperty("mail.enabled")控制,而application.yml里mail.enabled=false(默认关闭),且没有提供短信或站内信备用通道。
解决:
- 启用邮件:在
application-prod.yml中配置SMTP:
spring: mail: host: smtp.exmail.qq.com port: 465 username: hr@yourcompany.com password: your_app_password properties: mail: smtp: auth: true ssl: enable: true- 更稳妥方案:在
sendApprovalNotice()中增加降级逻辑:
if (!emailSender.isEnabled()) { log.warn("Email disabled, saving notice to t_notification table for manual push"); notificationMapper.insert(notification); // 存库,供后台定时任务扫描推送 }4.4 现象:导入员工Excel时,身份证号11010119900307231X被识别为科学计数法1.10101199E17,末尾X丢失
原因:Apache POI读取Excel时,对数字类型单元格默认转为Double,长数字(如身份证、银行卡号)精度丢失。源码中ExcelImportService.readEmployeeSheet()未对CellType.NUMERIC做特殊处理。
解决:
// 在读取单元格时,强制按字符串读 if (cell.getCellType() == CellType.NUMERIC) { // 关键:用DataFormatter避免科学计数法 DataFormatter formatter = new DataFormatter(Locale.CHINA); cellValue = formatter.formatCellValue(cell); } else { cellValue = cell.getStringCellValue(); } // 再trim()去空格,防止Excel里有看不见的空格 cellValue = cellValue.trim();4.5 现象:修改员工部门后,其下属员工的manager_id未级联更新,导致组织树错乱
原因:源码中EmployeeService.updateDepartment()方法只更新了employee.dept_id,但遗漏了UPDATE t_employee SET manager_id = ? WHERE manager_id = ?这条级联SQL。
解决:
- 在
EmployeeMapper.xml中添加级联更新SQL:
<update id="updateManagerByOldDept"> UPDATE t_employee SET manager_id = ( SELECT id FROM t_employee WHERE dept_id = #{newDeptId} AND position_level = 'DEPT_MANAGER' LIMIT 1 ) WHERE manager_id IN ( SELECT id FROM t_employee WHERE dept_id = #{oldDeptId} ) </update>- 在
EmployeeService.updateDepartment()中调用:
employeeMapper.updateManagerByOldDept(oldDeptId, newDeptId);血泪经验:HR系统里“部门变更”是高频操作,但90%的开源源码只处理了单点更新。务必检查所有涉及树形结构(部门、岗位、汇报关系)的修改接口,是否做了双向同步。
5. 权限体系加固:从“能登录”到“只能看该看的”的三道防线
HR数据敏感度极高,源码默认的ROLE_ADMIN/ROLE_USER两级权限远远不够。比如:HRBP能看本部门员工薪资,但不能看其他部门;部门负责人能审批本部门请假,但不能导出全员考勤明细。这就需要在源码里亲手焊上三道防线——数据层过滤、服务层校验、接口层拦截。
5.1 数据层:用MyBatis的@SelectProvider动态拼接租户和部门条件
很多源码的EmployeeMapper.selectList()是静态SQL:
<!-- 危险!所有人都能查全量 --> <select id="selectList" resultType="Employee"> SELECT * FROM t_employee </select>必须改造为动态SQL,注入当前用户可见的数据范围:
// EmployeeMapper.java @SelectProvider(type = EmployeeSqlProvider.class, method = "selectListWithScope") List<Employee> selectListWithScope(@Param("params") Map<String, Object> params); // EmployeeSqlProvider.java public class EmployeeSqlProvider { public String selectListWithScope(Map<String, Object> params) { return new SQL(){{ SELECT("*"); FROM("t_employee e"); // 【第一道防线】根据当前用户部门ID过滤 if (params.containsKey("currentDeptId")) { WHERE("e.dept_id = #{currentDeptId}"); } // 【第二道防线】根据租户ID(多公司场景)过滤 if (params.containsKey("tenantId")) { WHERE("e.tenant_id = #{tenantId}"); } // 【第三道防线】薪资字段脱敏:非HRBP且非本人,隐藏salary字段 if (!SecurityUtils.isHrBp() && !SecurityUtils.isSelf(params.get("userId"))) { // 不查salary列,或查出来也null SELECT("id, name, dept_id, position, entry_date"); } else { SELECT("*"); } }}.toString(); } }参数说明:
SecurityUtils是自定义工具类,从Spring Security的SecurityContextHolder.getContext().getAuthentication()中提取当前用户信息。isHrBp()通过查询用户角色表实现,isSelf()比对URL参数userId和当前登录用户ID。
5.2 服务层:在@Service方法上加@PreAuthorize注解,但必须配合自定义表达式
Spring Security的@PreAuthorize("hasRole('HRBP')")太粗放。我们需要更细粒度的控制,比如:“只能审批本部门员工的请假”。
步骤:
- 自定义权限表达式处理器:
@Component("permissionEvaluator") public class CustomPermissionEvaluator implements PermissionEvaluator { @Override public boolean hasPermission(Authentication auth, Object targetDomainObject, Object permission) { if (targetDomainObject instanceof LeaveApplication) { LeaveApplication app = (LeaveApplication) targetDomainObject; String currentDeptId = getCurrentUserDeptId(auth); return app.getEmployee().getDeptId().equals(currentDeptId); } return false; } }- 在Service方法上使用:
@Service public class ApprovalService { @PreAuthorize("@permissionEvaluator.hasPermission(authentication, #application, 'APPROVE')") public void approveLeave(LeaveApplication application) { // 执行审批逻辑 } }关键点:#application是SpEL表达式,代表方法参数,Spring会自动注入。这样,即使有人绕过前端,直接调用/api/v1/approval/leave接口,也会在进入方法前被拦截。
5.3 接口层:用@RequestMapping的headers属性做二次校验,防爬虫和越权探测
最后防线:在关键接口(如导出考勤、查看薪资)上,强制要求携带特定Header,该Header值由前端JS运行时生成(无法被curl直接复现):
@RestController @RequestMapping("/api/v1/report") public class ReportController { @GetMapping("/attendance/export") @RequestMapping(headers = "X-HR-Nonce=VALID") // 强制要求Header public ResponseEntity<Resource> exportAttendance( @RequestParam String deptId, @RequestHeader("X-HR-Timestamp") String timestamp, @RequestHeader("X-HR-Signature") String signature) { // 验证timestamp是否在5分钟内,signature是否为 HMAC-SHA256(timestamp + secretKey) if (!verifySignature(timestamp, signature)) { throw new AccessDeniedException("Invalid signature"); } // 执行导出 } }落地技巧:前端在点击“导出”按钮时,用JS生成:
const ts = Date.now().toString(); const sig = CryptoJS.HmacSHA256(ts, 'your-secret-key').toString(); axios.get('/api/v1/report/attendance/export', { headers: { 'X-HR-Nonce': 'VALID', 'X-HR-Timestamp': ts, 'X-HR-Signature': sig } });这样,即使攻击者抓到一次请求,5分钟后签名失效;且没有前端JS环境,无法生成合法signature。
6. 验证与上线 checklist:用5个真实业务场景跑通源码,才算真正“可用”
别信“编译通过”“页面能打开”。HRMIS源码的价值,只体现在它能否扛住真实业务压力。我给自己定的上线红线是:必须用公司最近一周的真实业务数据,在测试环境完整跑通以下5个场景,且每一步都有日志和数据库快照佐证。少一个,都不许上生产。
6.1 场景一:新员工入职全流程(数据创建一致性验证)
| 步骤 | 操作 | 验证点 | 日志/数据库证据 |
|---|---|---|---|
| 1 | 前端填写入职表单(姓名、身份证、部门、岗位、入职日期、试用期) | t_employee表是否新增记录?status字段是否为ONBOARDING? | SELECT * FROM t_employee WHERE id_card = '11010119900307231X' |
| 2 | 系统自动生成工号(规则:{deptCode}-{YYYY}{seq}) | 工号是否符合规则?deptCode是否取自部门表t_department.code? | SELECT emp_code, dept_id FROM t_employee WHERE id_card = '...' |
| 3 | 自动创建考勤排班(周一至周五 9:00-18:00) | t_attendance_schedule表是否生成5条记录?work_day字段是否为MONDAY~FRIDAY? | SELECT work_day, start_time, end_time FROM t_attendance_schedule WHERE emp_id = ? |
| 4 | 发送入职邮件(含账号密码、IT设备领取链接) | t_notification表是否新增一条type='ONBOARDING_EMAIL'记录?status是否为SENT? | SELECT type, status, content FROM t_notification WHERE emp_id = ? AND type = 'ONBOARDING_EMAIL' |
玄学提醒:很多源码的工号生成器用
SimpleDateFormat,在高并发下线程不安全,导致两个员工拿到相同工号。务必用ThreadLocal<SimpleDateFormat>或DateTimeFormatter(Java 8+)重写。
6.2 场景二:月度薪酬计算与发放(金额准确性验证)
用上月真实数据(假设:张三基本工资8000,绩效2000,五险一金个人部分1800,个税210):
| 步骤 | 操作 | 验证点 | 关键代码位置 |
|---|---|---|---|
| 1 | 运行SalaryCalculationJob(Quartz定时任务) | t_salary_record表是否生成一条记录?total_salary是否等于10000? | SalaryCalculator.java第142行result.setTotalSalary(base + bonus) |
| 2 | 手动触发个税计算 | tax_amount字段是否为210?检查TaxDeductionRule.groovy中税率表是否匹配2024年标准 | groovy/TaxDeductionRule.groovy第33行if (income <= 5000) { tax = 0 } |
| 3 | 执行银行代发(模拟) | t_bank_transfer表是否新增记录?status是否为SUCCESS?transfer_amount是否为8000-1800-210=5990? | BankTransferService.java第88行record.setTransferAmount(salaryRecord.getTotalSalary() - deduction) |
6.3 场景三:组织架构调整(数据完整性验证)
将“技术一部”整体划归到“研发中心”下(原属“产品中心”):
| 步骤 | 操作 | 验证点 | 必查SQL |
|---|---|---|---|
| 1 | 后台修改部门父级ID | t_department.parent_id是否更新?t_department.path字段(如/1/5/)是否重写为/1/3/5/? | SELECT id, parent_id, path FROM t_department WHERE code = 'TECH_DEPT_01' |
| 2 | 检查该部门下所有员工dept_id是否仍指向正确部门 | t_employee.dept_id是否批量更新? | SELECT COUNT(*) FROM t_employee WHERE dept_id = 5 AND status = 'ACTIVE' |
| 3 | 检查该部门负责人的manager_id是否指向“研发中心”负责人 | t_employee.manager_id是否级联更新? | SELECT manager_id FROM t_employee WHERE dept_id = 5 AND position = 'DEPT_MANAGER' |
避坑:
path字段用于快速查子部门,若未更新,SELECT * FROM t_department WHERE path LIKE '/1/3/5/%'将查不到任何子部门,导致组织树渲染失败。
6.4 场景四:员工离职与资产回收(状态机闭环验证)
发起张三离职申请 → HR审核 → IT确认设备归还 → 财务确认费用结清 → 状态变为LEAVED:
| 步骤 | 操作 | 验证点 | 状态流转关键点 |
|---|---|---|---|
| 1 | 提交离职申请 | t_resign_application.status是否为PENDING_HR? | ResignApplicationService.submit()中app.setStatus("PENDING_HR") |
| 2 | HR审核通过 | t_resign_application.status是否变为PENDING_IT?t_employee.status是否变为RESIGNING? | ResignApplicationService.approveByHR()中两处update |
| 3 | IT确认设备归还 | t_asset_return.status是否为COMPLETED?t_resign_application.status是否变为PENDING_FINANCE? | AssetReturnService.confirm()中resignApp.setStatus("PENDING_FINANCE") |
| 4 | 财务结清费用 | t_resign_application.status是否变为COMPLETED?t_employee.status是否变为LEAVED? | FinanceService.settle()中emp.setStatus("LEAVED") |
终极验证:t_employee.status = 'LEAVED'后,该员工是否从所有在职报表、考勤统计、组织树中消失?SELECT * FROM t_employee WHERE status = 'LEAVED'结果应仅包含历史离职员工。
6.5 场景五:权限边界测试(越权操作拦截验证)
用普通员工账号(非HR、非管理员)尝试以下操作:
| 操作 | 预期结果 | 实际结果 | 修复位置 |
|---|---|---|---|
访问/api/v1/salary/records?empId=1001(查别人薪资) | HTTP 403 Forbidden | 若返回200或404,说明权限失效 | SalaryController.getRecords()上加@PreAuthorize("isAuthenticated() and #empId == principal.id") |
POST/api/v1/department(新建部门) | HTTP 403 | 若返回400或500,说明没拦截到 | DepartmentController.create()上加@PreAuthorize("hasRole('HR_ADMIN')") |
GET/api/v1/report/attendance/export?deptId=all(导出全员考勤) | HTTP 403 | 若返回Excel文件,说明@RequestMapping(headers = "X-HR-Nonce=VALID")未生效 | 检查ReportController.exportAttendance()的@RequestMapping注解是否写错位置 |
我的习惯:上线前,我会用Postman新建一个“HRMIS-Go-Live”集合,把这5个场景的每一步请求都存为独立接口,设置好Headers、Body、Tests(断言HTTP状态码和响应JSON字段)。每次发布新版本,就点一下“Run Collection”,10分钟内知道能不能上。希望帮到你。
本文还有配套的精品资源,点击获取