AI 时代 HR 系统不能只建组织树:Spring Boot 考勤规则如何联动组织、岗位和人员变动

📅 2026/7/2 3:39:02
AI 时代 HR 系统不能只建组织树:Spring Boot 考勤规则如何联动组织、岗位和人员变动
现在很多企业系统都在谈 AI 自动化但如果底层组织、岗位、人员和规则没有建模清楚AI 也只能在错误数据上做总结。考勤系统就是典型例子员工调岗后仍按旧规则算考勤新员工没有及时纳入规则离职人员还在生成应打卡记录排班结束后没有回到常规规则最后 HR 月底继续手工修。这篇拆智慧考勤项目里的“组织岗位与规则联动”。代码来自 D:\workspace\ls_work\zhkq-project核心模块包括 zhkq-api 后端、zhkq-web 管理端和 zhkq-uniapp 移动端。文章只分析真实代码结构和工程经验不展示真实员工、真实组织、生产账号或内部配置。一、组织树不是通讯录考勤规则才是落地点企业后台通常都有组织树但组织树本身不能解决考勤。真正影响考勤结果的是这些关系对象考勤中的作用单位决定数据权限、规则归属和统计范围部门决定主管能看到哪些人员和记录岗位影响固定岗、外勤岗、值班岗、排班岗等规则选择人员最终绑定到具体考勤规则规则决定工作日、特殊工作日、休息日、时间段、区域和打卡方式如果系统只把规则绑定到部门会有明显问题。同一部门里可能有行政岗、外勤岗、值班岗、临时支援人员。部门级规则太粗人员级绑定才足够精细。项目里采用的是“规则表 规则人员关系表 规则时间关系表 规则区域关系表”的结构。新增规则时不只是保存一条规则还要同时写入人员、时间、区域关系void insertRuleAndConnection(KqAfRule kqAfRule) { // 新增规则与人关联 iKqRulePersonService.addByRuleIdAndPersonIds(kqAfRule.getId(), kqAfRule.getPersonId()); // 新增规则与时间关联 iKqRuleTimeService.addByRuleIdAndTimeIds(kqAfRule.getId(), kqAfRule.getTimeId()); // 如果是常规外勤并且考勤区域为空则不绑定区域 if (String.valueOf(ERuleType.LEGWORK.getValue()).equals(kqAfRule.getRuleType()) StrUtil.isBlank(kqAfRule.getAreaId())) { iKqRuleAreaService.delByRuleId(kqAfRule.getId()); return; } // 新增规则与区域关联 iKqRuleAreaService.addByRuleAndAreaIds(kqAfRule.getId(), kqAfRule.getAreaId()); }这段代码说明一个关键设计考勤规则不是孤立配置。规则必须同时落到人、时间和区域否则移动端查询规则时拿不到完整上下文。二、规则新增时先冻结审核通过后再生效新增常规考勤规则时系统先做基础校验规则名称不能重复特殊工作日和特殊休息日不能重叠常规考勤最多选择两个时间段并校验上下班时间是否冲突。Override public ResultString add(KqAfRule kqAfRule) { ListKqAfRule list this.queryList(kqAfRule); if (null ! list list.size() 0) { return Result.error(规则名称已存在); } verifySpecialDay(kqAfRule); kqAfRule.setAfStatus(AfStatusEnums.DRAFT.getValue()); kqAfRule.setAttendanceType(EAttendanceType.CG.getValue()); kqAfRule.setAttendanceTypeName(EAttendanceType.CG.getName()); kqAfRule.setUseStatus(RuleUseStatusEnums.NOT_USE.getValue()); verifyTimes(kqAfRule); this.save(kqAfRule); this.insertRuleAndConnection(kqAfRule); return Result.OK(添加成功); }人员关系新增时默认写成冻结状态Override public void addByRuleIdAndPersonIds(String ruleId, String personId) { if (StrUtil.isBlank(personId) || StrUtil.isBlank(ruleId)) { throw new RuntimeException(新增考勤规则与人关联失败,规则ID或人员ID为空); } this.delByRuleId(ruleId); ListKqRulePerson list new ArrayList(); String[] split personId.split(,); for (String s : split) { KqRulePerson kqRulePerson new KqRulePerson(); kqRulePerson.setRuleId(ruleId); kqRulePerson.setPersonId(s); kqRulePerson.setLockedStatus(CommonConstant.IS_LOCKED); list.add(kqRulePerson); } this.saveBatch(list); }这类设计比“保存即生效”更安全。考勤规则会影响员工出勤结论一旦错配月底统计和申诉都会受影响。规则先草稿、再审批、再生效可以把配置风险控制在流程里。三、编辑已完成规则时复制副本避免污染历史考勤规则最容易踩坑的地方是“编辑”。如果一个已经审核通过并使用过的规则被直接修改那么历史考勤解释就会变得不稳定。过去按 A 规则算出来的记录今天规则被改成 B历史页面再打开可能就解释不清。项目里的处理方式是已完成规则编辑时不直接覆盖而是复制一个新副本设置为草稿和未使用。Override public ResultString edit(KqAfRule kqAfRule) { KqAfRule oldRule this.getById(kqAfRule.getId()); if (!oldRule.getRuleName().equals(kqAfRule.getRuleName())) { return Result.error(不能修改规则名称); } if (AfStatusEnums.IN_SERVICE.getValue().equals(oldRule.getAfStatus())) { return Result.error(审核中); } if (AfStatusEnums.COMPLETED.getValue().equals(oldRule.getAfStatus())) { kqAfRule.setId(null); kqAfRule.setUseStatus(RuleUseStatusEnums.NOT_USE.getValue()); kqAfRule.setAfStatus(AfStatusEnums.DRAFT.getValue()); log.info(考勤规则审核状态为已完成,创建一个副本); } this.saveOrUpdate(kqAfRule); this.insertRuleAndConnection(kqAfRule); return Result.OK(编辑成功); }这是一个很实用的版本化思路规则名称保持稳定具体配置通过不同生效时间和状态切换。历史不被覆盖未来按新规则生效。四、生效时间由定时任务推动不靠人工点按钮规则审批通过后还不能简单立即替换所有旧规则。项目里有一个 XXL-JobUpdateRuleUseStatusJob。它按规则名称分组查询“审批通过、生效时间小于等于今天、生效时间最近”的规则然后把它切到使用中。XxlJob(value updateRuleUseStatusJob) Transactional(rollbackFor Exception.class, propagation Propagation.REQUIRED) public void execute() { ListKqAfRule kqAfRuleList kqAfRuleService.queryListByLastEffectiveTime(); ListKqAfRule collect kqAfRuleList.stream() .filter(kqAfRule - RuleUseStatusEnums.NOT_USE.getValue().equals(kqAfRule.getUseStatus())) .peek(kqAfRule - kqAfRule.setUseStatus(RuleUseStatusEnums.IN_USE.getValue())) .collect(Collectors.toList()); if (0 collect.size()) { log.info(没有规则需要更新使用状态); return; } kqAfRuleService.updateBatchById(collect); }Mapper 查询也体现了这个思路先找每个规则名称下最近的生效时间再按创建时间取最新版本。select idqueryListByLastEffectiveTime resultTypeorg.jeecg.modules.biz.entity.KqAfRule SELECT kq_af_rule.* FROM kq_af_rule INNER JOIN ( SELECT t1.rule_name, MAX(t1.create_time) AS create_time FROM kq_af_rule AS t1 INNER JOIN ( SELECT rule_name, MAX(effective_time) AS effective_time FROM kq_af_rule WHERE kq_af_rule.del_flag 0 AND kq_af_rule.af_status 2 AND kq_af_rule.use_status ! 2 AND kq_af_rule.effective_time CURDATE() GROUP BY rule_name ) AS t2 ON t1.rule_name t2.rule_name AND t1.effective_time t2.effective_time AND t1.del_flag 0 AND t1.af_status 2 AND t1.use_status ! 2 GROUP BY t1.rule_name ) AS t3 ON kq_af_rule.rule_name t3.rule_name AND kq_af_rule.create_time t3.create_time /select这一步对 HR 系统很关键。规则生效靠定时任务统一处理减少“今天该切规则但管理员忘了点”的风险。五、一个人只能有一条未冻结规则规则切换时系统会把新规则下人员的其他规则冻结然后把新规则与人关系解除冻结iKqRulePersonService.updateLockedStatusByPersonIdAndNeRuleId( kqRulePersonList.stream().map(KqRulePerson::getPersonId).collect(Collectors.toList()), rule.getId(), 1); iKqRulePersonService.updateLockedStatusByRuleIds(rule.getId(), 0);对应的 Service 实现是Override public void updateLockedStatusByPersonIdAndNeRuleId(ListString personId, String ruleid, Integer lockedStatus) { this.lambdaUpdate().in(KqRulePerson::getPersonId, personId) .ne(KqRulePerson::getRuleId, ruleid) .set(KqRulePerson::getLockedStatus, lockedStatus) .set(KqRulePerson::getUpdateTime, new Date()) .update(); }这个约束非常重要。真实企业里同一个人可能曾经绑定过多个规则老部门规则、新岗位规则、临时排班规则、值班规则。如果没有 locked_status 这种激活边界移动端查询当前规则时就会出现多条候选打卡结果不可预测。项目里当前规则查询就是按“人员 未冻结 审批通过 使用中”来找Override public KqAfRule queryKqInfoByUserId(String userId) { QueryWrapperKqAfRule queryWrapper new QueryWrapper(); queryWrapper.eq(kq_rule_person.person_id, userId) .eq(kq_rule_person.locked_status, CommonConstant.NOT_LOCKED) .eq(kq_af_rule.af_status, AfStatusEnums.COMPLETED.getValue()) .eq(kq_af_rule.use_status, RuleUseStatusEnums.IN_USE.getValue()); KqAfRule kqAfRule this.baseMapper.queryKqInfoByEw(queryWrapper); if (null kqAfRule) { throw new CustomException(暂无考勤规则); } return kqAfRule; }六、规则切换时要补齐上一条打卡UpdateRuleUseStatusJob 里还有一个容易被忽略的逻辑更换规则后根据人员查询最新的一条打卡记录如果最新是上班卡就把下班卡补上。for (KqRulePerson kqRulePerson : kqRulePersonList) { try { kqAttendanceRecordService.fillKqRecord(kqRulePerson.getPersonId()); } catch (Exception e) { e.printStackTrace(); log.info(fillKqRecord方法执行抛异常了,e{}, e.getMessage()); } }这说明规则联动不仅影响未来也可能影响当天闭环。如果一个人上午按旧规则上班下午规则切换系统要避免出现一条孤立的上班卡。否则月底统计会把它识别成异常HR 又要人工处理。从代码规范角度看这里还有改进空间项目规范要求用 log.error(异常信息, e) 替代 e.printStackTrace()。但从业务设计角度看“规则切换触发考勤补齐”是正确的。七、排班结束后要自动回到常规规则固定规则之外企业还会有临时排班和值班。排班规则结束后如果不回到常规规则员工后续打卡会继续按临时规则走。项目里有 BackTrackRuleJobXxlJob(value backTrackRuleJob) Transactional(rollbackFor Exception.class) public void execute() { ListDictModel dictItems sysDictService.getDictItems(back_track_rule); if (null dictItems || false.equals(dictItems.get(0).getValue())) { log.info(back_track_rule false); return; } ListKqRulePerson kqRulePersonList iKqRulePersonService.queryPbRuleCancel(); if (kqRulePersonList.size() 0) { log.info(没有执行完成的排班记录); return; } for (KqRulePerson kqRulePerson : kqRulePersonList) { kqRulePerson.setLockedStatus(CommonConstant.IS_LOCKED); } iKqRulePersonService.updateBatchById(kqRulePersonList); }随后它会查询这个人的最近一条常规考勤规则并解除冻结QueryWrapperKqAfRule w new QueryWrapper(); w.eq(kq_af_rule.af_status, AfStatusEnums.COMPLETED.getValue()) .eq(kq_af_rule.use_status, RuleUseStatusEnums.IN_USE.getValue()) .eq(kq_af_rule.attendance_type, EAttendanceType.CG.getValue()) .eq(kq_rule_person.person_id, kqRulePerson.getPersonId()) .orderByDesc(kq_af_rule.effective_time ) .last(LIMIT 1); KqAfRule kqAfRule kqAfRuleMapper.queryKqInfoByEw(w); if (null ! kqAfRule) { iKqRulePersonService.updateLockedStatusByPersonIdIdAndRuleId( kqRulePerson.getPersonId(), kqAfRule.getId(), CommonConstant.NOT_LOCKED); }这个任务本质上是“临时规则生命周期结束后的回滚机制”。如果没有它排班和值班功能很容易污染常规考勤。八、人员调动后规则详情不能继续显示旧人员工调单位或部门后历史规则里可能仍保留他的人员 ID。项目在查询规则详情时调用了 validateRulePerson判断人员是否已经不在当前规则所属单位下。public void validateRulePerson(KqAfRule rule) { SysDepart unit sysDepartService.getById(rule.getUnitId()); QueryWrapperSysUser queryWrapper new QueryWrapper(); queryWrapper.lambda() .in(SysUser::getId, Arrays.asList(rule.getPersonId().split(,))) .apply( org_code not like unit.getOrgCode() %); ListSysUser list sysUserService.list(queryWrapper); if (null ! list list.size() 0) { for (SysUser user : list) { rule.setPersonId(rule.getPersonId().replace(user.getId(), )); rule.setPersonName(rule.getPersonName().replace(user.getRealname(), )); } rule.setPersonId(rule.getPersonId().replace(,,, ,)); rule.setPersonName(rule.getPersonName().replace(,,, ,)); } }这段逻辑的目标是对的人员组织变动后规则详情不要继续把他当作当前单位的人。但实现上也有风险apply( org_code not like ...) 属于字符串拼接 SQL最好改成参数化写法避免后续组织编码异常时带来 SQL 风险字符串替换人员 ID 和姓名也容易留下首尾逗号后续可以改为集合过滤再 join。这种分析适合写在技术复盘里因为它不只是介绍功能也指出代码边界。九、管理端 API 是完整闭环PC 端规则页面接口在// zhkq-web/src/views/attendance/rule/rule.api.ts enum Api { list /biz/kqAfRule/list, handleDetail /biz/kqAfRule/queryById, save /biz/kqAfRule/addPlanRule, saveC /biz/kqAfRule/add, edit /biz/kqAfRule/edit, delete /biz/kqAfRule/delete, queryDepartTreeSync /biz/kqUserUnit/queryDepartTreeSync, afCommit /biz/kqAfRule/afCommit, addPrompt /biz/kqAfRule/addPrompt?personIds, getLegalList /biz/kqCalendar/queryScope }这里能看到规则闭环的几个关键入口1. 列表查规则。2. 新增常规规则。3. 新增排班/值班规则。4. 编辑规则。5. 提交审核。6. 校验人员是否已绑定其他规则。7. 查询法定节假日。8. 查询组织树。规则页面不是单纯 CRUD它连接了组织、人员、流程、日历、排班和权限。十、权限过滤不能后补规则列表查询里如果前端没有传 unitId后端会按当前用户能管理的单位做过滤if (StrUtil.isBlank(kqAfRule.getUnitId())) { ListSysDepart sysDeparts iKqUserUnitService.queryDeparts(DataAuthUtil.getCurrentUserId()); if (CollUtil.isNotEmpty(sysDeparts)) { if (sysDeparts.stream().anyMatch(dept - dept.getOrgCode().startsWith(A04))) { SysDepart a04 sysDepartService.queryDepartByOrgCode(A04); sysDeparts.add(a04); } queryWrapper.lambda().in(KqAfRule::getUnitId, sysDeparts.stream().map(SysDepart::getId).collect(Collectors.toList())); } }考勤规则包含人员范围、单位、区域、打卡方式。如果后台账号能看到全量规则就可能间接看到跨单位人员和排班信息。权限过滤必须在列表、导出、详情等入口保持一致。十一、这套设计的工程建议如果你要做企业考勤或 HR 系统可以按下面原则设计1. 规则不要只绑定部门最终要落到人员。2. 已使用规则不要直接覆盖编辑时生成新版本。3. 规则关系表要有冻结状态保证一个人只有一条当前规则。4. 规则生效靠定时任务按生效时间切换避免人工漏操作。5. 临时排班和值班结束后要自动回退常规规则。6. 人员调岗后要校验当前单位和规则人员范围。7. 列表和导出要按数据权限过滤。8. 日历、特殊工作日、特殊休息日必须和规则联动。9. 审批流程和规则生效要分离审批通过不等于立即使用。10. 规则切换要考虑当天未闭合打卡记录。AI 时代做企业系统不是把页面接上大模型就够了。真正影响系统质量的是底层业务对象是否稳定状态流转是否可追踪历史数据是否能解释。考勤规则联动组织、岗位和人员变动就是这种基础能力。基础打牢了后续无论做 AI 排班建议、异常自动解释还是 HR 月报自动生成才有可信的数据来源。