一、业务背景与挑战

1.1 企业考勤管理痛点

在企业考勤管理中,传统的固定打卡制度往往难以满足多样化的业务需求。不同的企业、不同的岗位、不同的工作方式,需要灵活的打卡规则。

传统考勤制度的问题

  • 一刀切问题:所有员工使用相同的打卡规则,缺乏灵活性
  • 员工体验差:严格的打卡时间导致员工满意度下降
  • 管理成本高:需要处理大量的异常打卡申请
  • 效率损失:员工为了不迟到而提前到岗,造成隐性工时浪费
  • 适应性差:无法适应弹性工作制、远程办公等新型工作方式

相关链接

在这里插入图片描述

1.2 灵活打卡的价值

什么是灵活打卡
灵活打卡是一种人性化的考勤管理制度,在保证工作时长和质量的前提下,允许员工在一定范围内灵活调整上下班时间。

灵活打卡的业务价值

  1. 提升员工满意度:减少打卡压力,改善工作体验
  2. 提高工作效率:员工可以在状态最佳时工作
  3. 降低管理成本:减少异常打卡处理的审批流程
  4. 增强组织适应性:适应弹性工作制和远程办公
  5. 保证工作质量:通过规则设计确保工作时长

在这里插入图片描述

1.3 技术实现挑战

灵活打卡的技术挑战

  1. 规则复杂多样:灵活对调、异常宽限、弹性时段等多种规则
  2. 计算逻辑复杂:需要考虑多种边界情况
  3. 性能要求高:打卡高峰期需要快速响应
  4. 数据一致性:确保打卡记录的准确性和完整性
  5. 规则可配置:支持企业自定义打卡规则


二、整体架构设计

2.1 系统架构全景

┌─────────────────────────────────────────────────────────────────┐
│                    灵活打卡规则引擎系统                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │                    规则配置管理层                          │   │
│  │  ┌──────────────┐  ┌──────────────┐  ┌─────────────────┐  │   │
│  │  │ 灵活对调规则 │  │ 异常宽限规则 │  │ 弹性时段规则    │  │   │
│  │  │(FlexibleSwap)│  │(GracePeriod) │  │(FlexibleTime)   │  │   │
│  │  └──────────────┘  └──────────────┘  └─────────────────┘  │   │
│  │  ┌──────────────┐  ┌──────────────┐  ┌─────────────────┐  │   │
│  │  │ 规则优先级   │  │ 规则冲突处理 │  │ 规则版本管理    │  │   │
│  │  │(Priority)    │  │(Conflict)    │  │(Version)        │  │   │
│  │  └──────────────┘  └──────────────┘  └─────────────────┘  │   │
│  └──────────────────────────────────────────────────────────┘   │
│                           │                                      │
│                           ▼                                      │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │                    规则计算引擎层                          │   │
│  │  ┌──────────────┐  ┌──────────────┐  ┌─────────────────┐  │   │
│  │  │ 规则调度器   │  │ 上下文管理器 │  │ 结果聚合器      │  │   │
│  │  │(Dispatcher)  │  │(Context)     │  │(Aggregator)     │  │   │
│  │  └──────────────┘  └──────────────┘  └─────────────────┘  │   │
│  │  ┌──────────────┐  ┌──────────────┐  ┌─────────────────┐  │   │
│  │  │ 规则执行器   │  │ 异常处理器   │  │ 日志记录器      │  │   │
│  │  │(Executor)    │  │(Handler)     │  │(Logger)         │  │   │
│  │  └──────────────┘  └──────────────┘  └─────────────────┘  │   │
│  └──────────────────────────────────────────────────────────┘   │
│                           │                                      │
│                           ▼                                      │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │                    打卡验证层                              │   │
│  │  ┌──────────────┐  ┌──────────────┐  ┌─────────────────┐  │   │
│  │  │ 时间验证     │  │ 位置验证     │  │ 设备验证        │  │   │
│  │  │(TimeCheck)   │  │(LocCheck)    │  │(DeviceCheck)    │  │   │
│  │  └──────────────┘  └──────────────┘  └─────────────────┘  │   │
│  │  ┌──────────────┐  ┌──────────────┐  ┌─────────────────┐  │   │
│  │  │ 状态计算     │  │ 异常标记     │  │ 规则匹配        │  │   │
│  │  │(Status)      │  │(Exception)   │  │(Matcher)        │  │   │
│  │  └──────────────┘  └──────────────┘  └─────────────────┘  │   │
│  └──────────────────────────────────────────────────────────┘   │
│                           │                                      │
│                           ▼                                      │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │                    数据存储层                              │   │
│  │  ┌──────────────┐  ┌──────────────┐  ┌─────────────────┐  │   │
│  │  │ 打卡记录表   │  │ 规则配置表   │  │ 计算历史表      │  │   │
│  │  │(ClockRecord) │  │(RuleConfig)  │  │(CalcHistory)    │  │   │
│  │  └──────────────┘  └──────────────┘  └─────────────────┘  │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

2.2 核心领域模型

灵活规则定义(AttendanceShiftFlexibleRuleDO)

/**
 * 班次灵活规则定义
 * 定义一个班次的灵活打卡规则
 */
@Data
@TableName("hrm_attendance_shift_flexible_rule")
public class AttendanceShiftFlexibleRuleDO {
    /**
     * 规则ID
     */
    private String id;
    
    /**
     * 班次ID
     */
    private String shiftId;
    
    /**
     * 规则类型
     * 1-灵活对调,2-异常宽限
     */
    private Integer ruleType;
    
    /**
     * 规则名称
     */
    private String ruleName;
    
    /**
     * 是否启用
     */
    private Boolean enabled;
    
    /**
     * 优先级(数值越小优先级越高)
     */
    private Integer priority;
    
    // ========== 灵活对调规则参数 ==========
    /**
     * 晚到上限(分钟)
     * 允许晚到的最大分钟数
     */
    private Integer maxLateArriveMinutes;
    
    /**
     * 早走上限(分钟)
     * 允许早走的最大分钟数
     */
    private Integer maxEarlyLeaveMinutes;
    
    // ========== 异常宽限规则参数 ==========
    /**
     * 迟到宽限时间(分钟)
     * 在此时间内迟到不算异常
     */
    private Integer lateGraceMinutes;
    
    /**
     * 早退宽限时间(分钟)
     * 在此时间内早退不算异常
     */
    private Integer earlyLeaveGraceMinutes;
    
    /**
     * 适用日期范围(JSON格式)
     * 示例:{"startDate":"2024-01-01","endDate":"2024-12-31"}
     */
    private String applicableDateRange;
    
    /**
     * 适用星期(JSON数组)
     * 示例:[1,2,3,4,5] 表示周一到周五
     */
    private String applicableWeekdays;
    
    /**
     * 排除日期(JSON数组)
     * 示例:["2024-01-01","2024-05-01"] 排除节假日
     */
    private String excludedDates;
    
    /**
     * 创建时间
     */
    private LocalDateTime createTime;
    
    /**
     * 更新时间
     */
    private LocalDateTime updateTime;
}

/**
 * 规则类型枚举
 */
public enum FlexibleRuleType {
    /**
     * 灵活对调规则(晚到晚走、早到早走)
     */
    FLEXIBLE_SWAP(1, "灵活对调"),
    
    /**
     * 异常宽限规则(宽限时间)
     */
    GRACE_PERIOD(2, "异常宽限");
    
    private final Integer code;
    private final String name;
    
    FlexibleRuleType(Integer code, String name) {
        this.code = code;
        this.name = name;
    }
    
    public Integer getCode() {
        return code;
    }
    
    public String getName() {
        return name;
    }
}

打卡记录模型(AttendanceDailyDO)

/**
 * 考勤日报记录
 */
@Data
@TableName("hrm_attendance_daily")
public class AttendanceDailyDO {
    /**
     * 记录ID
     */
    private String id;
    
    /**
     * 员工ID
     */
    private String employeeId;
    
    /**
     * 考勤日期
     */
    private LocalDate attendanceDate;
    
    /**
     * 班次ID
     */
    private String shiftId;
    
    /**
     * 班次名称
     */
    private String shiftName;
    
    // ========== 打卡时间 ==========
    /**
     * 上班打卡时间
     */
    private LocalDateTime signInTime;
    
    /**
     * 下班打卡时间
     */
    private LocalDateTime signOutTime;
    
    /**
     * 打卡位置(JSON格式)
     */
    private String location;
    
    /**
     * 打卡设备
     */
    private String device;
    
    // ========== 标准时间 ==========
    /**
     * 标准上班时间
     */
    private LocalTime standardSignInTime;
    
    /**
     * 标准下班时间
     */
    private LocalTime standardSignOutTime;
    
    // ========== 计算结果 ==========
    /**
     * 签到状态:0-正常,1-迟到,2-未签到
     */
    private String signInStatus;
    
    /**
     * 签退状态:0-正常,1-早退,2-未签退
     */
    private String signOutStatus;
    
    /**
     * 迟到分钟数
     */
    private Integer lateMinutes;
    
    /**
     * 早退分钟数
     */
    private Integer earlyMinutes;
    
    /**
     * 工作时长(小时)
     */
    private BigDecimal workHours;
    
    /**
     * 加班时长(小时)
     */
    private BigDecimal overtimeDuration;
    
    /**
     * 应出勤状态:1-正常出勤,2-请假,3-出差,4-缺勤
     */
    private Integer attendanceStatus;
    
    /**
     * 使用的规则ID
     */
    private String appliedRuleId;
    
    /**
     * 使用的规则类型
     */
    private Integer appliedRuleType;
    
    /**
     * 计算详情(JSON格式)
     */
    private String calculationDetail;
    
    /**
     * 创建时间
     */
    private LocalDateTime createTime;
    
    /**
     * 更新时间
     */
    private LocalDateTime updateTime;
}

2.3 规则引擎架构设计

灵活规则计算引擎接口

/**
 * 灵活打卡规则计算引擎
 * 定义灵活打卡规则计算的统一接口
 */
public interface FlexibleRuleCalculationEngine {
    
    /**
     * 应用所有灵活规则计算打卡结果
     * 
     * @param actualSignInTime 实际签到时间
     * @param actualSignOutTime 实际签退时间
     * @param standardSignInTime 标准签到时间
     * @param standardSignOutTime 标准签退时间
     * @param flexibleRules 灵活规则列表
     * @return 打卡计算结果
     */
    AttendanceResult calculateWithAllRules(
        LocalTime actualSignInTime,
        LocalTime actualSignOutTime,
        LocalTime standardSignInTime,
        LocalTime standardSignOutTime,
        List<AttendanceShiftFlexibleRuleDO> flexibleRules
    );
    
    /**
     * 应用单个规则计算打卡结果
     * 
     * @param actualSignInTime 实际签到时间
     * @param actualSignOutTime 实际签退时间
     * @param standardSignInTime 标准签到时间
     * @param standardSignOutTime 标准签退时间
     * @param rule 灵活规则
     * @return 打卡计算结果
     */
    AttendanceResult calculateWithSingleRule(
        LocalTime actualSignInTime,
        LocalTime actualSignOutTime,
        LocalTime standardSignInTime,
        LocalTime standardSignOutTime,
        AttendanceShiftFlexibleRuleDO rule
    );
    
    /**
     * 验证规则是否适用
     * 
     * @param rule 灵活规则
     * @param attendanceDate 考勤日期
     * @return 是否适用
     */
    boolean isRuleApplicable(
        AttendanceShiftFlexibleRuleDO rule,
        LocalDate attendanceDate
    );
}

/**
 * 打卡计算结果
 */
@Data
@Builder
public class AttendanceResult {
    /**
     * 签到状态:0-正常,1-迟到,2-未签到
     */
    private String signInStatus;
    
    /**
     * 签退状态:0-正常,1-早退,2-未签退
     */
    private String signOutStatus;
    
    /**
     * 签到是否正常
     */
    private Boolean signInNormal;
    
    /**
     * 签退是否正常
     */
    private Boolean signOutNormal;
    
    /**
     * 迟到分钟数
     */
    private Integer lateMinutes;
    
    /**
     * 早退分钟数
     */
    private Integer earlyMinutes;
    
    /**
     * 应工作时长(分钟)
     */
    private Integer requiredWorkMinutes;
    
    /**
     * 实际工作时长(分钟)
     */
    private Integer actualWorkMinutes;
    
    /**
     * 使用的规则ID
     */
    private String appliedRuleId;
    
    /**
     * 使用的规则类型
     */
    private Integer appliedRuleType;
    
    /**
     * 计算详情(JSON格式)
     */
    private String calculationDetail;
    
    /**
     * 是否为正常出勤
     */
    public boolean isNormalAttendance() {
        return Boolean.TRUE.equals(signInNormal) && 
               Boolean.TRUE.equals(signOutNormal);
    }
    
    /**
     * 获取状态描述
     */
    public String getStatusDescription() {
        List<String> statusList = new ArrayList<>();
        
        if ("2".equals(signInStatus)) {
            statusList.add("未签到");
        } else if (!signInNormal) {
            statusList.add("迟到" + lateMinutes + "分钟");
        }
        
        if ("2".equals(signOutStatus)) {
            statusList.add("未签退");
        } else if (!signOutNormal) {
            statusList.add("早退" + earlyMinutes + "分钟");
        }
        
        if (statusList.isEmpty()) {
            return "正常";
        }
        
        return String.join("、", statusList);
    }
}

三、核心模块实现

3.1 灵活对调规则实现

灵活对调规则计算器

/**
 * 灵活对调规则计算器
 * 
 * 规则说明:
 * - 晚到晚走:上班晚到一定时间,下班相应晚走同样时间,不算迟到
 * - 早到早走:上班早到一定时间,下班相应早走,不算早退
 * - 保证工作时长:核心是保证实际工作时长不低于标准工作时长
 * 
 * 适用场景:
 * - 弹性工作制
 * - 项目型工作
 * - 结果导向型岗位
 */
@Component
@Slf4j
public class FlexibleSwapRuleCalculator {
    
    /**
     * 计算灵活对调规则的打卡结果
     * 
     * @param actualSignInTime 实际签到时间
     * @param actualSignOutTime 实际签退时间
     * @param standardSignInTime 标准签到时间
     * @param standardSignOutTime 标准签退时间
     * @param rule 灵活对调规则
     * @return 打卡计算结果
     */
    public AttendanceResult calculate(
        LocalTime actualSignInTime,
        LocalTime actualSignOutTime,
        LocalTime standardSignInTime,
        LocalTime standardSignOutTime,
        AttendanceShiftFlexibleRuleDO rule
    ) {
        // 1. 计算迟到早退分钟数
        int lateMinutes = calculateLateMinutes(actualSignInTime, standardSignInTime);
        int earlyLeaveMinutes = calculateEarlyLeaveMinutes(
            actualSignOutTime, standardSignOutTime
        );
        
        // 2. 计算标准工作时长
        int standardWorkMinutes = calculateStandardWorkMinutes(
            standardSignInTime, standardSignOutTime
        );
        
        // 3. 计算实际工作时长
        int actualWorkMinutes = calculateActualWorkMinutes(
            actualSignInTime, actualSignOutTime
        );
        
        AttendanceResult result = AttendanceResult.builder()
            .requiredWorkMinutes(standardWorkMinutes)
            .actualWorkMinutes(actualWorkMinutes)
            .appliedRuleId(rule.getId())
            .appliedRuleType(rule.getRuleType())
            .build();
        
        // 4. 判断签到状态
        if (lateMinutes > 0) {
            // 晚到了
            if (lateMinutes > rule.getMaxLateArriveMinutes()) {
                // 超过晚到上限
                result.setSignInStatus("1");
                result.setSignInNormal(false);
                result.setLateMinutes(lateMinutes);
            } else {
                // 晚到在上限内,需要检查是否相应晚走
                if (actualWorkMinutes >= standardWorkMinutes) {
                    // 实际工作时长达标,记为正常
                    result.setSignInStatus("0");
                    result.setSignInNormal(true);
                    result.setLateMinutes(0);
                } else {
                    // 实际工作时长不足,记为迟到(不足部分)
                    int lackMinutes = standardWorkMinutes - actualWorkMinutes;
                    result.setSignInStatus("1");
                    result.setSignInNormal(false);
                    result.setLateMinutes(lateMinutes - 
                        (earlyLeaveMinutes < 0 ? -earlyLeaveMinutes : 0));
                }
            }
        } else if (lateMinutes < 0) {
            // 早到了,记为正常
            result.setSignInStatus("0");
            result.setSignInNormal(true);
            result.setLateMinutes(0);
        } else {
            // 准时
            result.setSignInStatus("0");
            result.setSignInNormal(true);
            result.setLateMinutes(0);
        }
        
        // 5. 判断签退状态
        if (earlyLeaveMinutes > 0) {
            // 早走了
            if (earlyLeaveMinutes > rule.getMaxEarlyLeaveMinutes()) {
                // 超过早走上限
                result.setSignOutStatus("1");
                result.setSignOutNormal(false);
                result.setEarlyMinutes(earlyLeaveMinutes);
            } else {
                // 早走在上限内,需要检查是否相应早到
                if (actualWorkMinutes >= standardWorkMinutes) {
                    // 实际工作时长达标,记为正常
                    result.setSignOutStatus("0");
                    result.setSignOutNormal(true);
                    result.setEarlyMinutes(0);
                } else {
                    // 实际工作时长不足,记为早退(不足部分)
                    int lackMinutes = standardWorkMinutes - actualWorkMinutes;
                    result.setSignOutStatus("1");
                    result.setSignOutNormal(false);
                    result.setEarlyMinutes(earlyLeaveMinutes - 
                        (lateMinutes < 0 ? -lateMinutes : 0));
                }
            }
        } else if (earlyLeaveMinutes < 0) {
            // 晚走了,记为正常
            result.setSignOutStatus("0");
            result.setSignOutNormal(true);
            result.setEarlyMinutes(0);
        } else {
            // 准时
            result.setSignOutStatus("0");
            result.setSignOutNormal(true);
            result.setEarlyMinutes(0);
        }
        
        // 6. 记录计算详情
        Map<String, Object> detail = new HashMap<>();
        detail.put("lateMinutes", lateMinutes);
        detail.put("earlyLeaveMinutes", earlyLeaveMinutes);
        detail.put("standardWorkMinutes", standardWorkMinutes);
        detail.put("actualWorkMinutes", actualWorkMinutes);
        detail.put("maxLateArriveMinutes", rule.getMaxLateArriveMinutes());
        detail.put("maxEarlyLeaveMinutes", rule.getMaxEarlyLeaveMinutes());
        result.setCalculationDetail(JSON.toJSONString(detail));
        
        return result;
    }
    
    /**
     * 计算迟到分钟数
     * 
     * @param actualTime 实际时间
     * @param standardTime 标准时间
     * @return 迟到分钟数(正数表示迟到,负数表示早到)
     */
    private int calculateLateMinutes(LocalTime actualTime, LocalTime standardTime) {
        return (int) ChronoUnit.MINUTES.between(standardTime, actualTime);
    }
    
    /**
     * 计算早退分钟数
     * 
     * @param actualTime 实际时间
     * @param standardTime 标准时间
     * @return 早退分钟数(正数表示早退,负数表示晚走)
     */
    private int calculateEarlyLeaveMinutes(LocalTime actualTime, LocalTime standardTime) {
        return (int) ChronoUnit.MINUTES.between(actualTime, standardTime);
    }
    
    /**
     * 计算标准工作时长(分钟)
     */
    private int calculateStandardWorkMinutes(LocalTime signInTime, LocalTime signOutTime) {
        return (int) ChronoUnit.MINUTES.between(signInTime, signOutTime);
    }
    
    /**
     * 计算实际工作时长(分钟)
     */
    private int calculateActualWorkMinutes(LocalTime signInTime, LocalTime signOutTime) {
        if (signInTime == null || signOutTime == null) {
            return 0;
        }
        return (int) ChronoUnit.MINUTES.between(signInTime, signOutTime);
    }
}

3.2 异常宽限规则实现

异常宽限规则计算器

/**
 * 异常宽限规则计算器
 * 
 * 规则说明:
 * - 宽限时间:在宽限时间内的迟到/早退不计为异常
 * - 不保证时长:宽限规则不保证工作时长,只放宽异常判定标准
 * 
 * 适用场景:
 * - 交通拥堵容忍
 * - 临时性短时延误
 * - 人性化管理
 */
@Component
@Slf4j
public class GracePeriodRuleCalculator {
    
    /**
     * 计算异常宽限规则的打卡结果
     * 
     * @param actualSignInTime 实际签到时间
     * @param actualSignOutTime 实际签退时间
     * @param standardSignInTime 标准签到时间
     * @param standardSignOutTime 标准签退时间
     * @param rule 异常宽限规则
     * @return 打卡计算结果
     */
    public AttendanceResult calculate(
        LocalTime actualSignInTime,
        LocalTime actualSignOutTime,
        LocalTime standardSignInTime,
        LocalTime standardSignOutTime,
        AttendanceShiftFlexibleRuleDO rule
    ) {
        // 1. 计算迟到早退分钟数
        int lateMinutes = calculateLateMinutes(actualSignInTime, standardSignInTime);
        int earlyLeaveMinutes = calculateEarlyLeaveMinutes(
            actualSignOutTime, standardSignOutTime
        );
        
        AttendanceResult result = AttendanceResult.builder()
            .appliedRuleId(rule.getId())
            .appliedRuleType(rule.getRuleType())
            .build();
        
        // 2. 判断签到状态(应用宽限规则)
        if (lateMinutes > 0) {
            if (lateMinutes <= rule.getLateGraceMinutes()) {
                // 在宽限期内,不算迟到
                result.setSignInStatus("0");
                result.setSignInNormal(true);
                result.setLateMinutes(0);
            } else {
                // 超过宽限期,记为迟到
                result.setSignInStatus("1");
                result.setSignInNormal(false);
                result.setLateMinutes(lateMinutes);
            }
        } else if (lateMinutes < 0) {
            // 早到了
            result.setSignInStatus("0");
            result.setSignInNormal(true);
            result.setLateMinutes(0);
        } else {
            // 准时
            result.setSignInStatus("0");
            result.setSignInNormal(true);
            result.setLateMinutes(0);
        }
        
        // 3. 判断签退状态(应用宽限规则)
        if (earlyLeaveMinutes > 0) {
            if (earlyLeaveMinutes <= rule.getEarlyLeaveGraceMinutes()) {
                // 在宽限期内,不算早退
                result.setSignOutStatus("0");
                result.setSignOutNormal(true);
                result.setEarlyMinutes(0);
            } else {
                // 超过宽限期,记为早退
                result.setSignOutStatus("1");
                result.setSignOutNormal(false);
                result.setEarlyMinutes(earlyLeaveMinutes);
            }
        } else if (earlyLeaveMinutes < 0) {
            // 晚走了
            result.setSignOutStatus("0");
            result.setSignOutNormal(true);
            result.setEarlyMinutes(0);
        } else {
            // 准时
            result.setSignOutStatus("0");
            result.setSignOutNormal(true);
            result.setEarlyMinutes(0);
        }
        
        // 4. 记录计算详情
        Map<String, Object> detail = new HashMap<>();
        detail.put("lateMinutes", lateMinutes);
        detail.put("earlyLeaveMinutes", earlyLeaveMinutes);
        detail.put("lateGraceMinutes", rule.getLateGraceMinutes());
        detail.put("earlyLeaveGraceMinutes", rule.getEarlyLeaveGraceMinutes());
        result.setCalculationDetail(JSON.toJSONString(detail));
        
        return result;
    }
    
    /**
     * 计算迟到分钟数
     */
    private int calculateLateMinutes(LocalTime actualTime, LocalTime standardTime) {
        return (int) ChronoUnit.MINUTES.between(standardTime, actualTime);
    }
    
    /**
     * 计算早退分钟数
     */
    private int calculateEarlyLeaveMinutes(LocalTime actualTime, LocalTime standardTime) {
        return (int) ChronoUnit.MINUTES.between(actualTime, standardTime);
    }
}

3.3 规则调度引擎实现

规则调度引擎

/**
 * 灵活打卡规则调度引擎
 * 负责协调多个规则的执行顺序和结果聚合
 */
@Service
@Slf4j
public class FlexibleRuleDispatchEngine implements FlexibleRuleCalculationEngine {
    
    @Autowired
    private FlexibleSwapRuleCalculator swapRuleCalculator;
    
    @Autowired
    private GracePeriodRuleCalculator gracePeriodRuleCalculator;
    
    /**
     * 应用所有灵活规则计算打卡结果
     * 
     * 执行顺序:
     * 1. 优先应用异常宽限规则(rule_type=2)
     * 2. 如果宽限规则判断为正常,直接返回
     * 3. 否则应用灵活对调规则(rule_type=1)
     * 4. 如果都没有,使用标准计算
     * 
     * @param actualSignInTime 实际签到时间
     * @param actualSignOutTime 实际签退时间
     * @param standardSignInTime 标准签到时间
     * @param standardSignOutTime 标准签退时间
     * @param flexibleRules 灵活规则列表
     * @return 打卡计算结果
     */
    @Override
    public AttendanceResult calculateWithAllRules(
        LocalTime actualSignInTime,
        LocalTime actualSignOutTime,
        LocalTime standardSignInTime,
        LocalTime standardSignOutTime,
        List<AttendanceShiftFlexibleRuleDO> flexibleRules
    ) {
        // 1. 处理没有灵活规则的情况
        if (flexibleRules == null || flexibleRules.isEmpty()) {
            return calculateStandard(
                actualSignInTime, actualSignOutTime,
                standardSignInTime, standardSignOutTime
            );
        }
        
        // 2. 过滤出启用的规则
        List<AttendanceShiftFlexibleRuleDO> enabledRules = flexibleRules.stream()
            .filter(AttendanceShiftFlexibleRuleDO::getEnabled)
            .sorted(Comparator.comparing(
                AttendanceShiftFlexibleRuleDO::getPriority))
            .collect(Collectors.toList());
        
        if (enabledRules.isEmpty()) {
            return calculateStandard(
                actualSignInTime, actualSignOutTime,
                standardSignInTime, standardSignOutTime
            );
        }
        
        // 3. 优先应用异常宽限规则
        AttendanceShiftFlexibleRuleDO graceRule = findRuleByType(
            enabledRules, FlexibleRuleType.GRACE_PERIOD.getCode()
        );
        
        if (graceRule != null) {
            AttendanceResult result = gracePeriodRuleCalculator.calculate(
                actualSignInTime, actualSignOutTime,
                standardSignInTime, standardSignOutTime, graceRule
            );
            
            // 如果宽限规则判断为正常,直接返回
            if (result.isNormalAttendance()) {
                log.debug("异常宽限规则判定为正常: ruleId={}", graceRule.getId());
                return result;
            }
            
            log.debug("异常宽限规则判定为异常: ruleId={}, status={}", 
                graceRule.getId(), result.getStatusDescription());
        }
        
        // 4. 应用灵活对调规则
        AttendanceShiftFlexibleRuleDO swapRule = findRuleByType(
            enabledRules, FlexibleRuleType.FLEXIBLE_SWAP.getCode()
        );
        
        if (swapRule != null) {
            AttendanceResult result = swapRuleCalculator.calculate(
                actualSignInTime, actualSignOutTime,
                standardSignInTime, standardSignOutTime, swapRule
            );
            
            log.debug("灵活对调规则计算完成: ruleId={}, status={}", 
                swapRule.getId(), result.getStatusDescription());
            return result;
        }
        
        // 5. 没有适用的规则,使用标准计算
        return calculateStandard(
            actualSignInTime, actualSignOutTime,
            standardSignInTime, standardSignOutTime
        );
    }
    
    /**
     * 应用单个规则计算打卡结果
     */
    @Override
    public AttendanceResult calculateWithSingleRule(
        LocalTime actualSignInTime,
        LocalTime actualSignOutTime,
        LocalTime standardSignInTime,
        LocalTime standardSignOutTime,
        AttendanceShiftFlexibleRuleDO rule
    ) {
        if (rule == null || !rule.getEnabled()) {
            return calculateStandard(
                actualSignInTime, actualSignOutTime,
                standardSignInTime, standardSignOutTime
            );
        }
        
        // 根据规则类型选择计算器
        if (FlexibleRuleType.FLEXIBLE_SWAP.getCode().equals(rule.getRuleType())) {
            return swapRuleCalculator.calculate(
                actualSignInTime, actualSignOutTime,
                standardSignInTime, standardSignOutTime, rule
            );
        } else if (FlexibleRuleType.GRACE_PERIOD.getCode().equals(rule.getRuleType())) {
            return gracePeriodRuleCalculator.calculate(
                actualSignInTime, actualSignOutTime,
                standardSignInTime, standardSignOutTime, rule
            );
        }
        
        // 未知规则类型,使用标准计算
        return calculateStandard(
            actualSignInTime, actualSignOutTime,
            standardSignInTime, standardSignOutTime
        );
    }
    
    /**
     * 验证规则是否适用
     */
    @Override
    public boolean isRuleApplicable(
        AttendanceShiftFlexibleRuleDO rule,
        LocalDate attendanceDate
    ) {
        if (rule == null || !rule.getEnabled()) {
            return false;
        }
        
        // 1. 检查日期范围
        if (!isWithinApplicableDateRange(rule, attendanceDate)) {
            return false;
        }
        
        // 2. 检查星期
        if (!isWithinApplicableWeekdays(rule, attendanceDate)) {
            return false;
        }
        
        // 3. 检查排除日期
        if (isExcludedDate(rule, attendanceDate)) {
            return false;
        }
        
        return true;
    }
    
    /**
     * 标准计算(无灵活规则)
     */
    private AttendanceResult calculateStandard(
        LocalTime actualSignInTime,
        LocalTime actualSignOutTime,
        LocalTime standardSignInTime,
        LocalTime standardSignOutTime
    ) {
        AttendanceResult result = AttendanceResult.builder().build();
        
        // 判断签到状态
        if (actualSignInTime == null) {
            result.setSignInStatus("2"); // 未签到
            result.setSignInNormal(false);
        } else if (actualSignInTime.isAfter(standardSignInTime)) {
            // 迟到
            int lateMinutes = (int) ChronoUnit.MINUTES.between(
                standardSignInTime, actualSignInTime
            );
            result.setSignInStatus("1");
            result.setSignInNormal(false);
            result.setLateMinutes(lateMinutes);
        } else {
            // 正常或早到
            result.setSignInStatus("0");
            result.setSignInNormal(true);
            result.setLateMinutes(0);
        }
        
        // 判断签退状态
        if (actualSignOutTime == null) {
            result.setSignOutStatus("2"); // 未签退
            result.setSignOutNormal(false);
        } else if (actualSignOutTime.isBefore(standardSignOutTime)) {
            // 早退
            int earlyMinutes = (int) ChronoUnit.MINUTES.between(
                actualSignOutTime, standardSignOutTime
            );
            result.setSignOutStatus("1");
            result.setSignOutNormal(false);
            result.setEarlyMinutes(earlyMinutes);
        } else {
            // 正常或晚走
            result.setSignOutStatus("0");
            result.setSignOutNormal(true);
            result.setEarlyMinutes(0);
        }
        
        return result;
    }
    
    /**
     * 根据规则类型查找规则
     */
    private AttendanceShiftFlexibleRuleDO findRuleByType(
        List<AttendanceShiftFlexibleRuleDO> rules, Integer ruleType
    ) {
        return rules.stream()
            .filter(rule -> ruleType.equals(rule.getRuleType()))
            .findFirst()
            .orElse(null);
    }
    
    /**
     * 检查是否在适用日期范围内
     */
    private boolean isWithinApplicableDateRange(
        AttendanceShiftFlexibleRuleDO rule,
        LocalDate attendanceDate
    ) {
        String dateRangeStr = rule.getApplicableDateRange();
        if (StringUtils.isBlank(dateRangeStr)) {
            return true; // 没有配置日期范围,默认适用
        }
        
        try {
            JSONObject dateRange = JSON.parseObject(dateRangeStr);
            LocalDate startDate = LocalDate.parse(dateRange.getString("startDate"));
            LocalDate endDate = LocalDate.parse(dateRange.getString("endDate"));
            
            return !attendanceDate.isBefore(startDate) && 
                   !attendanceDate.isAfter(endDate);
        } catch (Exception e) {
            log.warn("解析适用日期范围失败: ruleId={}, error={}", 
                rule.getId(), e.getMessage());
            return true; // 解析失败,默认适用
        }
    }
    
    /**
     * 检查是否在适用星期内
     */
    private boolean isWithinApplicableWeekdays(
        AttendanceShiftFlexibleRuleDO rule,
        LocalDate attendanceDate
    ) {
        String weekdaysStr = rule.getApplicableWeekdays();
        if (StringUtils.isBlank(weekdaysStr)) {
            return true; // 没有配置星期,默认适用
        }
        
        try {
            List<Integer> weekdays = JSON.parseArray(weekdaysStr, Integer.class);
            DayOfWeek dayOfWeek = attendanceDate.getDayOfWeek();
            
            return weekdays.contains(dayOfWeek.getValue());
        } catch (Exception e) {
            log.warn("解析适用星期失败: ruleId={}, error={}", 
                rule.getId(), e.getMessage());
            return true; // 解析失败,默认适用
        }
    }
    
    /**
     * 检查是否为排除日期
     */
    private boolean isExcludedDate(
        AttendanceShiftFlexibleRuleDO rule,
        LocalDate attendanceDate
    ) {
        String excludedDatesStr = rule.getExcludedDates();
        if (StringUtils.isBlank(excludedDatesStr)) {
            return false; // 没有配置排除日期
        }
        
        try {
            List<LocalDate> excludedDates = JSON.parseArray(
                excludedDatesStr, LocalDate.class
            );
            
            return excludedDates.contains(attendanceDate);
        } catch (Exception e) {
            log.warn("解析排除日期失败: ruleId={}, error={}", 
                rule.getId(), e.getMessage());
            return false; // 解析失败,不排除
        }
    }
}

3.4 位置验证实现

位置验证服务

/**
 * 位置打卡验证服务
 * 
 * 核心功能:
 * 1. 验证打卡位置是否在允许的范围内
 * 2. 使用Haversine公式计算两点间距离
 * 3. 支持组织级和考勤组级的位置配置
 * 4. 支持配置打卡半径
 */
@Service
@Slf4j
public class LocationValidationService {
    
    @Autowired
    private AttendanceModeService attendanceModeService;
    
    /**
     * 默认打卡半径(米)
     */
    private static final double DEFAULT_RADIUS = 400;
    
    /**
     * 地球半径(米)
     */
    private static final double EARTH_RADIUS = 6371000;
    
    /**
     * 验证位置打卡(组织模式)
     * 
     * @param configCoordinateX 配置的经度
     * @param configCoordinateY 配置的纬度
     * @param configRadius 配置的打卡半径(米)
     * @param actualCoordinateX 实际打卡经度
     * @param actualCoordinateY 实际打卡纬度
     * @throws RuntimeException 验证失败抛出异常
     */
    public void validateLocationForOrgMode(
        Double configCoordinateX,
        Double configCoordinateY,
        Double configRadius,
        String actualCoordinateX,
        String actualCoordinateY
    ) {
        // 1. 检查系统配置是否启用位置打卡
        if (!attendanceModeService.isLocationCheckEnabled()) {
            return; // 系统未启用位置打卡,不验证
        }
        
        // 2. 检查是否配置了打卡位置
        if (configCoordinateX == null || configCoordinateY == null) {
            return; // 未配置位置,不验证
        }
        
        // 3. 检查用户是否传入了位置信息
        if (StringUtils.isBlank(actualCoordinateX) || 
            StringUtils.isBlank(actualCoordinateY)) {
            throw new LocationValidationException("请提供位置信息");
        }
        
        // 4. 计算距离并验证
        double actualX = Double.parseDouble(actualCoordinateX);
        double actualY = Double.parseDouble(actualCoordinateY);
        double distance = calculateDistance(
            actualX, actualY, configCoordinateX, configCoordinateY
        );
        
        double radius = configRadius != null ? configRadius : DEFAULT_RADIUS;
        
        if (distance > radius) {
            throw new LocationValidationException(
                String.format("当前位置不在打卡范围内,距离%.0f米", distance)
            );
        }
        
        log.debug("位置验证通过: distance={}", distance);
    }
    
    /**
     * 验证位置打卡(考勤组模式)
     * 
     * @param groupCoordinateX 考勤组配置的经度
     * @param groupCoordinateY 考勤组配置的纬度
     * @param groupRadius 考勤组配置的打卡半径(米)
     * @param actualCoordinateX 实际打卡经度
     * @param actualCoordinateY 实际打卡纬度
     * @throws RuntimeException 验证失败抛出异常
     */
    public void validateLocationForGroupMode(
        Double groupCoordinateX,
        Double groupCoordinateY,
        Double groupRadius,
        String actualCoordinateX,
        String actualCoordinateY
    ) {
        // 1. 检查系统配置是否启用位置打卡
        if (!attendanceModeService.isLocationCheckEnabled()) {
            return; // 系统未启用位置打卡,不验证
        }
        
        // 2. 检查考勤组是否配置了位置
        if (groupCoordinateX == null || groupCoordinateY == null) {
            return; // 考勤组未配置位置,不验证
        }
        
        // 3. 检查用户是否传入了位置信息
        if (StringUtils.isBlank(actualCoordinateX) || 
            StringUtils.isBlank(actualCoordinateY)) {
            throw new LocationValidationException("请提供位置信息");
        }
        
        // 4. 计算距离并验证
        double actualX = Double.parseDouble(actualCoordinateX);
        double actualY = Double.parseDouble(actualCoordinateY);
        double distance = calculateDistance(
            actualX, actualY, groupCoordinateX, groupCoordinateY
        );
        
        double radius = groupRadius != null ? groupRadius : DEFAULT_RADIUS;
        
        if (distance > radius) {
            throw new LocationValidationException(
                String.format("当前位置不在打卡范围内,距离%.0f米", distance)
            );
        }
        
        log.debug("位置验证通过: distance={}", distance);
    }
    
    /**
     * 计算两点之间的距离(米)
     * 
     * 使用 Haversine 公式计算地球表面两点之间的距离
     * 
     * Haversine公式:
     * a = sin²(Δφ/2) + cos(φ1) ⋅ cos(φ2) ⋅ sin²(Δλ/2)
     * c = 2 ⋅ atan2(√a, √(1−a))
     * d = R ⋅ c
     * 
     * 其中:
     * - φ1, φ2: 两点的纬度
     * - λ1, λ2: 两点的经度
     * - Δφ = φ2 - φ1
     * - Δλ = λ2 - λ1
     * - R: 地球半径(6371000米)
     * - d: 两点间的距离
     * 
     * @param longitude1 经度1
     * @param latitude1 纬度1
     * @param longitude2 经度2
     * @param latitude2 纬度2
     * @return 距离(米)
     */
    public double calculateDistance(
        double longitude1,
        double latitude1,
        double longitude2,
        double latitude2
    ) {
        // 1. 将角度转换为弧度
        double lat1Rad = Math.toRadians(latitude1);
        double lat2Rad = Math.toRadians(latitude2);
        double deltaLonRad = Math.toRadians(longitude2 - longitude1);
        double deltaLatRad = Math.toRadians(latitude2 - latitude1);
        
        // 2. 应用Haversine公式
        double a = Math.sin(deltaLatRad / 2) * Math.sin(deltaLatRad / 2) +
                   Math.cos(lat1Rad) * Math.cos(lat2Rad) *
                   Math.sin(deltaLonRad / 2) * Math.sin(deltaLonRad / 2);
        
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        
        // 3. 计算距离
        return EARTH_RADIUS * c;
    }
    
    /**
     * 批量验证位置
     * 
     * @param locations 位置列表
     * @param configCoordinates 配置坐标
     * @param radius 半径
     * @return 验证结果列表
     */
    public List<LocationValidationResult> validateBatch(
        List<LocationPoint> locations,
        LocationPoint configCoordinates,
        Double radius
    ) {
        return locations.stream()
            .map(location -> {
                try {
                    validateLocationForOrgMode(
                        configCoordinates.getLongitude(),
                        configCoordinates.getLatitude(),
                        radius,
                        String.valueOf(location.getLongitude()),
                        String.valueOf(location.getLatitude())
                    );
                    return LocationValidationResult.success(location);
                } catch (LocationValidationException e) {
                    return LocationValidationResult.failure(location, e.getMessage());
                }
            })
            .collect(Collectors.toList());
    }
    
    /**
     * 计算多边形中心点
     * 用于设置多边形打卡区域的中心位置
     * 
     * @param points 多边形顶点列表
     * @return 中心点坐标
     */
    public LocationPoint calculatePolygonCenter(List<LocationPoint> points) {
        if (points == null || points.isEmpty()) {
            throw new IllegalArgumentException("多边形顶点不能为空");
        }
        
        double sumX = 0;
        double sumY = 0;
        
        for (LocationPoint point : points) {
            sumX += point.getLongitude();
            sumY += point.getLatitude();
        }
        
        return new LocationPoint(
            sumX / points.size(),
            sumY / points.size()
        );
    }
    
    /**
     * 判断点是否在多边形内
     * 用于多边形打卡区域的判定
     * 
     * @param point 判定点
     * @param polygon 多边形顶点列表
     * @return 是否在多边形内
     */
    public boolean isPointInPolygon(LocationPoint point, List<LocationPoint> polygon) {
        if (polygon == null || polygon.size() < 3) {
            return false;
        }
        
        int crossings = 0;
        int n = polygon.size();
        
        for (int i = 0; i < n; i++) {
            LocationPoint p1 = polygon.get(i);
            LocationPoint p2 = polygon.get((i + 1) % n);
            
            // 检查射线是否与边相交
            if ((p1.getLatitude() > point.getLatitude()) != 
                (p2.getLatitude() > point.getLatitude())) {
                
                // 计算射线与边的交点的x坐标
                double intersectX = (point.getLatitude() - p1.getLatitude()) * 
                    (p2.getLongitude() - p1.getLongitude()) / 
                    (p2.getLatitude() - p1.getLatitude()) + p1.getLongitude();
                
                if (point.getLongitude() < intersectX) {
                    crossings++;
                }
            }
        }
        
        // 奇数个交点表示在多边形内
        return (crossings % 2) == 1;
    }
}

/**
 * 位置点
 */
@Data
@AllArgsConstructor
public class LocationPoint {
    /**
     * 经度
     */
    private Double longitude;
    
    /**
     * 纬度
     */
    private Double latitude;
}

/**
 * 位置验证结果
 */
@Data
@Builder
public class LocationValidationResult {
    /**
     * 位置点
     */
    private LocationPoint location;
    
    /**
     * 是否验证通过
     */
    private Boolean valid;
    
    /**
     * 错误信息
     */
    private String errorMessage;
    
    public static LocationValidationResult success(LocationPoint location) {
        return LocationValidationResult.builder()
            .location(location)
            .valid(true)
            .build();
    }
    
    public static LocationValidationResult failure(LocationPoint location, String errorMessage) {
        return LocationValidationResult.builder()
            .location(location)
            .valid(false)
            .errorMessage(errorMessage)
            .build();
    }
}

/**
 * 位置验证异常
 */
public class LocationValidationException extends RuntimeException {
    public LocationValidationException(String message) {
        super(message);
    }
}

3.5 打卡流程集成

打卡服务集成

/**
 * 考勤打卡服务
 * 整合灵活规则引擎和位置验证,提供完整的打卡功能
 */
@Service
@Slf4j
public class AttendanceClockInService {
    
    @Autowired
    private FlexibleRuleDispatchEngine ruleEngine;
    
    @Autowired
    private LocationValidationService locationValidationService;
    
    @Autowired
    private AttendanceDailyService attendanceDailyService;
    
    /**
     * 执行签到
     * 
     * @param request 签到请求
     * @return 签到结果
     */
    @Transactional(rollbackFor = Exception.class)
    public ClockInResult clockIn(ClockInRequest request) {
        log.info("执行签到: employeeId={}, time={}", 
            request.getEmployeeId(), request.getClockInTime());
        
        // 1. 获取员工考勤组和班次信息
        AttendanceGroupDO group = attendanceGroupService.getByEmployeeId(request.getEmployeeId());
        if (group == null) {
            throw new BusinessException("员工未分配考勤组");
        }
        
        AttendanceShiftDO shift = attendanceShiftService.getById(group.getShiftId());
        if (shift == null) {
            throw new BusinessException("考勤组未分配班次");
        }
        
        // 2. 验证打卡位置
        try {
            locationValidationService.validateLocationForGroupMode(
                group.getLongitude(),
                group.getLatitude(),
                group.getLocationRadius(),
                request.getLocationX(),
                request.getLocationY()
            );
        } catch (LocationValidationException e) {
            // 位置验证失败
            log.warn("签到位置验证失败: employeeId={}, error={}", 
                request.getEmployeeId(), e.getMessage());
            
            // 根据配置决定是否允许打卡
            if (!Boolean.TRUE.equals(group.getAllowClockInOutsideLocation())) {
                throw new BusinessException(e.getMessage());
            }
        }
        
        // 3. 获取灵活规则
        List<AttendanceShiftFlexibleRuleDO> flexibleRules = 
            attendanceShiftFlexibleRuleService.listByShiftId(shift.getId());
        
        // 4. 应用灵活规则计算签到状态
        LocalTime actualSignInTime = request.getClockInTime().toLocalTime();
        LocalTime actualSignOutTime = null; // 签到时没有签退时间
        
        AttendanceResult attendanceResult = ruleEngine.calculateWithAllRules(
            actualSignInTime,
            actualSignOutTime,
            shift.getSignInTime(), // 标准上班时间
            shift.getSignOutTime(), // 标准下班时间
            flexibleRules
        );
        
        // 5. 保存签到记录
        AttendanceDailyDO dailyRecord = new AttendanceDailyDO();
        dailyRecord.setId(IdUtil.fastUUID());
        dailyRecord.setEmployeeId(request.getEmployeeId());
        dailyRecord.setAttendanceDate(request.getClockInTime().toLocalDate());
        dailyRecord.setShiftId(shift.getId());
        dailyRecord.setShiftName(shift.getShiftName());
        dailyRecord.setSignInTime(request.getClockInTime());
        dailyRecord.setLocation(buildLocationString(request.getLocationX(), request.getLocationY()));
        dailyRecord.setDevice(request.getDevice());
        dailyRecord.setStandardSignInTime(shift.getSignInTime());
        dailyRecord.setStandardSignOutTime(shift.getSignOutTime());
        dailyRecord.setSignInStatus(attendanceResult.getSignInStatus());
        dailyRecord.setLateMinutes(attendanceResult.getLateMinutes());
        
        // 如果已有当天记录,更新;否则插入
        attendanceDailyService.saveOrUpdate(dailyRecord);
        
        // 6. 构建返回结果
        ClockInResult result = ClockInResult.builder()
            .success(true)
            .signInStatus(attendanceResult.getSignInStatus())
            .signInNormal(attendanceResult.getSignInNormal())
            .lateMinutes(attendanceResult.getLateMinutes())
            .message(attendanceResult.getSignInNormal() ? "签到成功" : 
                "签到成功,但" + attendanceResult.getStatusDescription())
            .build();
        
        log.info("签到完成: employeeId={}, status={}, lateMinutes={}", 
            request.getEmployeeId(), result.getSignInStatus(), result.getLateMinutes());
        
        return result;
    }
    
    /**
     * 执行签退
     * 
     * @param request 签退请求
     * @return 签退结果
     */
    @Transactional(rollbackFor = Exception.class)
    public ClockOutResult clockOut(ClockOutRequest request) {
        log.info("执行签退: employeeId={}, time={}", 
            request.getEmployeeId(), request.getClockOutTime());
        
        // 1. 获取当天的签到记录
        LocalDate attendanceDate = request.getClockOutTime().toLocalDate();
        AttendanceDailyDO dailyRecord = attendanceDailyService.getByEmployeeAndDate(
            request.getEmployeeId(), attendanceDate
        );
        
        if (dailyRecord == null) {
            throw new BusinessException("未找到今日签到记录");
        }
        
        // 2. 获取班次和灵活规则
        AttendanceShiftDO shift = attendanceShiftService.getById(dailyRecord.getShiftId());
        List<AttendanceShiftFlexibleRuleDO> flexibleRules = 
            attendanceShiftFlexibleRuleService.listByShiftId(shift.getId());
        
        // 3. 应用灵活规则计算签退状态
        LocalTime actualSignInTime = dailyRecord.getSignInTime() != null ? 
            dailyRecord.getSignInTime().toLocalTime() : null;
        LocalTime actualSignOutTime = request.getClockOutTime().toLocalTime();
        
        AttendanceResult attendanceResult = ruleEngine.calculateWithAllRules(
            actualSignInTime,
            actualSignOutTime,
            shift.getSignInTime(),
            shift.getSignOutTime(),
            flexibleRules
        );
        
        // 4. 更新签退记录
        dailyRecord.setSignOutTime(request.getClockOutTime());
        dailyRecord.setLocation(buildLocationString(request.getLocationX(), request.getLocationY()));
        dailyRecord.setDevice(request.getDevice());
        dailyRecord.setSignOutStatus(attendanceResult.getSignOutStatus());
        dailyRecord.setEarlyMinutes(attendanceResult.getEarlyMinutes());
        
        // 计算工作时长
        if (actualSignInTime != null && actualSignOutTime != null) {
            int workMinutes = (int) ChronoUnit.MINUTES.between(actualSignInTime, actualSignOutTime);
            dailyRecord.setWorkHours(new BigDecimal(workMinutes)
                .divide(new BigDecimal("60"), 2, RoundingMode.HALF_UP));
        }
        
        attendanceDailyService.updateById(dailyRecord);
        
        // 5. 构建返回结果
        ClockOutResult result = ClockOutResult.builder()
            .success(true)
            .signOutStatus(attendanceResult.getSignOutStatus())
            .signOutNormal(attendanceResult.getSignOutNormal())
            .earlyMinutes(attendanceResult.getEarlyMinutes())
            .workHours(dailyRecord.getWorkHours())
            .message(attendanceResult.getSignOutNormal() ? "签退成功" : 
                "签退成功,但" + attendanceResult.getStatusDescription())
            .build();
        
        log.info("签退完成: employeeId={}, status={}, earlyMinutes={}, workHours={}", 
            request.getEmployeeId(), result.getSignOutStatus(), 
            result.getEarlyMinutes(), result.getWorkHours());
        
        return result;
    }
    
    /**
     * 构建位置字符串
     */
    private String buildLocation(String longitude, String latitude) {
        if (StringUtils.isBlank(longitude) || StringUtils.isBlank(latitude)) {
            return null;
        }
        
        Map<String, String> location = new HashMap<>();
        location.put("longitude", longitude);
        location.put("latitude", latitude);
        
        return JSON.toJSONString(location);
    }
}

四、高级特性实现

4.1 规则冲突处理

规则冲突处理器

/**
 * 灵活规则冲突处理器
 * 处理多个规则同时适用时的冲突问题
 */
@Component
@Slf4j
public class FlexibleRuleConflictHandler {
    
    /**
     * 解决规则冲突
     * 
     * 策略:
     * 1. 按优先级排序,优先级高的规则优先生效
     * 2. 如果优先级相同,按规则类型排序:宽限规则 > 对调规则
     * 3. 支持规则组合:多个规则可以同时生效
     * 
     * @param rules 灵活规则列表
     * @return 解决冲突后的规则列表
     */
    public List<AttendanceShiftFlexibleRuleDO> resolveConflicts(
        List<AttendanceShiftFlexibleRuleDO> rules
    ) {
        if (rules == null || rules.size() <= 1) {
            return rules;
        }
        
        // 1. 过滤出启用的规则
        List<AttendanceShiftFlexibleRuleDO> enabledRules = rules.stream()
            .filter(AttendanceShiftFlexibleRuleDO::getEnabled)
            .collect(Collectors.toList());
        
        if (enabledRules.isEmpty()) {
            return Collections.emptyList();
        }
        
        // 2. 按优先级排序
        enabledRules.sort(Comparator.comparing(
            AttendanceShiftFlexibleRuleDO::getPriority
        ));
        
        // 3. 检查是否有相同优先级的规则
        Map<Integer, List<AttendanceShiftFlexibleRuleDO>> groupedByPriority = 
            enabledRules.stream()
                .collect(Collectors.groupingBy(
                    AttendanceShiftFlexibleRuleDO::getPriority
                ));
        
        // 4. 处理相同优先级的规则
        List<AttendanceShiftFlexibleRuleDO> resolvedRules = new ArrayList<>();
        
        for (Map.Entry<Integer, List<AttendanceShiftFlexibleRuleDO>> entry : 
                groupedByPriority.entrySet()) {
            
            List<AttendanceShiftFlexibleRuleDO> samePriorityRules = entry.getValue();
            
            if (samePriorityRules.size() == 1) {
                // 没有冲突
                resolvedRules.add(samePriorityRules.get(0));
            } else {
                // 有冲突,选择规则类型优先级更高的
                AttendanceShiftFlexibleRuleDO selectedRule = 
                    selectRuleByType(samePriorityRules);
                resolvedRules.add(selectedRule);
                
                log.debug("规则冲突解决: priority={}, selectedRule={}, excluded={}", 
                    entry.getKey(), selectedRule.getId(),
                    samePriorityRules.stream()
                        .filter(r -> !r.getId().equals(selectedRule.getId()))
                        .map(AttendanceShiftFlexibleRuleDO::getId)
                        .collect(Collectors.toList())
                );
            }
        }
        
        return resolvedRules;
    }
    
    /**
     * 根据规则类型选择优先级更高的规则
     * 
     * 规则类型优先级:
     * 1. 异常宽限规则(rule_type=2)优先
     * 2. 灵活对调规则(rule_type=1)次之
     */
    private AttendanceShiftFlexibleRuleDO selectRuleByType(
        List<AttendanceShiftFlexibleRuleDO> rules
    ) {
        // 优先选择异常宽限规则
        AttendanceShiftFlexibleRuleDO graceRule = rules.stream()
            .filter(rule -> FlexibleRuleType.GRACE_PERIOD.getCode().equals(rule.getRuleType()))
            .findFirst()
            .orElse(null);
        
        if (graceRule != null) {
            return graceRule;
        }
        
        // 其次选择灵活对调规则
        return rules.stream()
            .filter(rule -> FlexibleRuleType.FLEXIBLE_SWAP.getCode().equals(rule.getRuleType()))
            .findFirst()
            .orElse(rules.get(0));
    }
    
    /**
     * 检测规则冲突
     * 
     * @param rules 规则列表
     * @return 冲突检测结果
     */
    public ConflictDetectionResult detectConflicts(
        List<AttendanceShiftFlexibleRuleDO> rules
    ) {
        ConflictDetectionResult result = ConflictDetectionResult.builder().build();
        
        if (rules == null || rules.size() <= 1) {
            return result;
        }
        
        // 检测相同优先级的规则
        Map<Integer, List<AttendanceShiftFlexibleRuleDO>> groupedByPriority = 
            rules.stream()
                .filter(AttendanceShiftFlexibleRuleDO::getEnabled)
                .collect(Collectors.groupingBy(
                    AttendanceShiftFlexibleRuleDO::getPriority
                ));
        
        List<ConflictGroup> conflicts = new ArrayList<>();
        
        for (Map.Entry<Integer, List<AttendanceShiftFlexibleRuleDO>> entry : 
                groupedByPriority.entrySet()) {
            
            List<AttendanceShiftFlexibleRuleDO> samePriorityRules = entry.getValue();
            
            if (samePriorityRules.size() > 1) {
                // 检测到冲突
                ConflictGroup conflict = ConflictGroup.builder()
                    .priority(entry.getKey())
                    .conflictingRules(samePriorityRules)
                    .conflictCount(samePriorityRules.size())
                    .build();
                
                conflicts.add(conflict);
            }
        }
        
        result.setConflicts(conflicts);
        result.setHasConflicts(!conflicts.isEmpty());
        
        return result;
    }
}

/**
 * 冲突检测结果
 */
@Data
@Builder
public class ConflictDetectionResult {
    /**
     * 是否有冲突
     */
    private Boolean hasConflicts;
    
    /**
     * 冲突组列表
     */
    private List<ConflictGroup> conflicts;
}

/**
 * 冲突组
 */
@Data
@Builder
public class ConflictGroup {
    /**
     * 优先级
     */
    private Integer priority;
    
    /**
     * 冲突的规则列表
     */
    private List<AttendanceShiftFlexibleRuleDO> conflictingRules;
    
    /**
     * 冲突数量
     */
    private Integer conflictCount;
}

4.2 规则版本管理

规则版本管理服务

/**
 * 灵活规则版本管理服务
 * 支持规则的版本控制和历史追溯
 */
@Service
@Slf4j
public class FlexibleRuleVersionService {
    
    @Autowired
    private AttendanceShiftFlexibleRuleMapper ruleMapper;
    
    @Autowired
    private FlexibleRuleChangeLogMapper changeLogMapper;
    
    /**
     * 创建规则快照
     * 
     * @param shiftId 班次ID
     * @param version 版本号
     * @return 快照ID
     */
    @Transactional(rollbackFor = Exception.class)
    public String createSnapshot(String shiftId, String version) {
        log.info("创建规则快照: shiftId={}, version={}", shiftId, version);
        
        // 1. 获取当前规则
        List<AttendanceShiftFlexibleRuleDO> currentRules = ruleMapper.selectList(
            new LambdaQueryWrapper<AttendanceShiftFlexibleRuleDO>()
                .eq(AttendanceShiftFlexibleRuleDO::getShiftId, shiftId)
                .eq(AttendanceShiftFlexibleRuleDO::getEnabled, true)
        );
        
        // 2. 创建快照
        List<FlexibleRuleSnapshot> snapshots = new ArrayList<>();
        
        for (AttendanceShiftFlexibleRuleDO rule : currentRules) {
            FlexibleRuleSnapshot snapshot = new FlexibleRuleSnapshot();
            snapshot.setId(IdUtil.fastUUID());
            snapshot.setShiftId(shiftId);
            snapshot.setRuleId(rule.getId());
            snapshot.setRuleType(rule.getRuleType());
            snapshot.setRuleName(rule.getRuleName());
            snapshot.setRuleConfig(JSON.toJSONString(rule));
            snapshot.setVersion(version);
            snapshot.setSnapshotTime(LocalDateTime.now());
            
            snapshots.add(snapshot);
        }
        
        // 3. 批量保存快照
        if (!snapshots.isEmpty()) {
            flexibleRuleSnapshotMapper.insertBatch(snapshots);
        }
        
        log.info("规则快照创建完成: shiftId={}, version={}, count={}", 
            shiftId, version, snapshots.size());
        
        return version;
    }
    
    /**
     * 恢复规则版本
     * 
     * @param shiftId 班次ID
     * @param version 目标版本
     * @return 恢复的规则数量
     */
    @Transactional(rollbackFor = Exception.class)
    public int restoreVersion(String shiftId, String version) {
        log.info("恢复规则版本: shiftId={}, version={}", shiftId, version);
        
        // 1. 获取目标版本的快照
        List<FlexibleRuleSnapshot> snapshots = flexibleRuleSnapshotMapper.selectList(
            new LambdaQueryWrapper<FlexibleRuleSnapshot>()
                .eq(FlexibleRuleSnapshot::getShiftId, shiftId)
                .eq(FlexibleRuleSnapshot::getVersion, version)
        );
        
        if (snapshots.isEmpty()) {
            throw new BusinessException("未找到指定版本的规则快照");
        }
        
        // 2. 删除当前规则
        ruleMapper.delete(
            new LambdaQueryWrapper<AttendanceShiftFlexibleRuleDO>()
                .eq(AttendanceShiftFlexibleRuleDO::getShiftId, shiftId)
        );
        
        // 3. 从快照恢复规则
        int restoredCount = 0;
        
        for (FlexibleRuleSnapshot snapshot : snapshots) {
            try {
                AttendanceShiftFlexibleRuleDO rule = JSON.parseObject(
                    snapshot.getRuleConfig(),
                    AttendanceShiftFlexibleRuleDO.class
                );
                
                rule.setId(IdUtil.fastUUID()); // 生成新ID
                rule.setCreateTime(LocalDateTime.now());
                rule.setUpdateTime(LocalDateTime.now());
                
                ruleMapper.insert(rule);
                restoredCount++;
                
            } catch (Exception e) {
                log.error("恢复规则失败: ruleId={}, error={}", 
                    snapshot.getRuleId(), e.getMessage());
            }
        }
        
        // 4. 记录变更日志
        recordChangeLog(shiftId, "RESTORE_VERSION", 
            String.format("恢复到版本%s,共恢复%d条规则", version, restoredCount));
        
        log.info("规则版本恢复完成: shiftId={}, version={}, count={}", 
            shiftId, version, restoredCount);
        
        return restoredCount;
    }
    
    /**
     * 对比两个版本的规则差异
     * 
     * @param shiftId 班次ID
     * @param version1 版本1
     * @param version2 版本2
     * @return 差异对比结果
     */
    public RuleDiffResult compareVersions(
        String shiftId,
        String version1,
        String version2
    ) {
        log.info("对比规则版本: shiftId={}, v1={}, v2={}", shiftId, version1, version2);
        
        // 1. 获取两个版本的快照
        List<FlexibleRuleSnapshot> snapshots1 = flexibleRuleSnapshotMapper.selectList(
            new LambdaQueryWrapper<FlexibleRuleSnapshot>()
                .eq(FlexibleRuleSnapshot::getShiftId, shiftId)
                .eq(FlexibleRuleSnapshot::getVersion, version1)
        );
        
        List<FlexibleRuleSnapshot> snapshots2 = flexibleRuleSnapshotMapper.selectList(
            new LambdaQueryWrapper<FlexibleRuleSnapshot>()
                .eq(FlexibleRuleSnapshot::getShiftId, shiftId)
                .eq(FlexibleRuleSnapshot::getVersion, version2)
        );
        
        // 2. 构建差异结果
        RuleDiffResult diffResult = RuleDiffResult.builder()
            .shiftId(shiftId)
            .version1(version1)
            .version2(version2)
            .build();
        
        // 3. 找出新增的规则(在v2中有,v1中没有)
        List<RuleDifference> addedRules = snapshots2.stream()
            .filter(s2 -> snapshots1.stream()
                .noneMatch(s1 -> s1.getRuleId().equals(s2.getRuleId())))
            .map(s2 -> RuleDifference.builder()
                .ruleType("ADDED")
                .ruleId(s2.getRuleId())
                .ruleName(s2.getRuleName())
                .version2Value(s2.getRuleConfig())
                .build())
            .collect(Collectors.toList());
        
        diffResult.setAddedRules(addedRules);
        
        // 4. 找出删除的规则(在v1中有,v2中没有)
        List<RuleDifference> removedRules = snapshots1.stream()
            .filter(s1 -> snapshots2.stream()
                .noneMatch(s2 -> s2.getRuleId().equals(s1.getRuleId())))
            .map(s1 -> RuleDifference.builder()
                .ruleType("REMOVED")
                .ruleId(s1.getRuleId())
                .ruleName(s1.getRuleName())
                .version1Value(s1.getRuleConfig())
                .build())
            .collect(Collectors.toList());
        
        diffResult.setRemovedRules(removedRules);
        
        // 5. 找出修改的规则
        List<RuleDifference> modifiedRules = new ArrayList<>();
        
        for (FlexibleRuleSnapshot s1 : snapshots1) {
            FlexibleRuleSnapshot s2 = snapshots2.stream()
                .filter(snapshot -> snapshot.getRuleId().equals(s1.getRuleId()))
                .findFirst()
                .orElse(null);
            
            if (s2 != null && !s1.getRuleConfig().equals(s2.getRuleConfig())) {
                modifiedRules.add(RuleDifference.builder()
                    .ruleType("MODIFIED")
                    .ruleId(s1.getRuleId())
                    .ruleName(s1.getRuleName())
                    .version1Value(s1.getRuleConfig())
                    .version2Value(s2.getRuleConfig())
                    .build());
            }
        }
        
        diffResult.setModifiedRules(modifiedRules);
        
        // 6. 设置统计信息
        diffResult.setTotalAdded(addedRules.size());
        diffResult.setTotalRemoved(removedRules.size());
        diffResult.setTotalModified(modifiedRules.size());
        
        log.info("规则版本对比完成: shiftId={}, added={}, removed={}, modified={}", 
            shiftId, addedRules.size(), removedRules.size(), modifiedRules.size());
        
        return diffResult;
    }
    
    /**
     * 记录变更日志
     */
    private void recordChangeLog(String shiftId, String changeType, String description) {
        FlexibleRuleChangeLog changeLog = new FlexibleRuleChangeLog();
        changeLog.setId(IdUtil.fastUUID());
        changeLog.setShiftId(shiftId);
        changeLog.setChangeType(changeType);
        changeLog.setDescription(description);
        changeLog.setChangeTime(LocalDateTime.now());
        changeLog.setOperator(SecurityContextHolder.getUserId());
        
        changeLogMapper.insert(changeLog);
    }
}

/**
 * 规则差异结果
 */
@Data
@Builder
public class RuleDiffResult {
    private String shiftId;
    private String version1;
    private String version2;
    private List<RuleDifference> addedRules;
    private List<RuleDifference> removedRules;
    private List<RuleDifference> modifiedRules;
    private Integer totalAdded;
    private Integer totalRemoved;
    private Integer totalModified;
}

/**
 * 规则差异
 */
@Data
@Builder
public class RuleDifference {
    private String ruleType; // ADDED, REMOVED, MODIFIED
    private String ruleId;
    private String ruleName;
    private String version1Value;
    private String version2Value;
}

五、性能优化

5.1 打卡高峰优化

打卡性能优化策略

/**
 * 打卡性能优化服务
 * 应对打卡高峰期的性能压力
 */
@Service
@Slf4j
public class ClockInPerformanceOptimizer {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private FlexibleRuleDispatchEngine ruleEngine;
    
    /**
     * 预加载灵活规则到缓存
     * 
     * @param shiftIds 班次ID列表
     */
    public void preloadRulesToCache(List<String> shiftIds) {
        log.info("预加载灵活规则到缓存: shiftCount={}", shiftIds.size());
        
        for (String shiftId : shiftIds) {
            List<AttendanceShiftFlexibleRuleDO> rules = 
                attendanceShiftFlexibleRuleMapper.selectList(
                    new LambdaQueryWrapper<AttendanceShiftFlexibleRuleDO>()
                        .eq(AttendanceShiftFlexibleRuleDO::getShiftId, shiftId)
                        .eq(AttendanceShiftFlexibleRuleDO::getEnabled, true)
                        .orderByAsc(AttendanceShiftFlexibleRuleDO::getPriority)
                );
            
            if (!rules.isEmpty()) {
                String cacheKey = buildRuleCacheKey(shiftId);
                redisTemplate.opsForValue().set(cacheKey, rules, 1, TimeUnit.HOURS);
            }
        }
        
        log.info("灵活规则预加载完成");
    }
    
    /**
     * 从缓存获取规则
     * 
     * @param shiftId 班次ID
     * @return 灵活规则列表
     */
    public List<AttendanceShiftFlexibleRuleDO> getRulesFromCache(String shiftId) {
        String cacheKey = buildRuleCacheKey(shiftId);
        
        Object cached = redisTemplate.opsForValue().get(cacheKey);
        
        if (cached != null) {
            return (List<AttendanceShiftFlexibleRuleDO>) cached;
        }
        
        // 缓存未命中,从数据库加载
        List<AttendanceShiftFlexibleRuleDO> rules = 
            attendanceShiftFlexibleRuleMapper.selectList(
                new LambdaQueryWrapper<AttendanceShiftFlexibleRuleDO>()
                    .eq(AttendanceShiftFlexibleRuleDO::getShiftId, shiftId)
                    .eq(AttendanceShiftFlexibleRuleDO::getEnabled, true)
                    .orderByAsc(AttendanceShiftFlexibleRuleDO::getPriority)
            );
        
        // 写入缓存
        if (!rules.isEmpty()) {
            redisTemplate.opsForValue().set(cacheKey, rules, 1, TimeUnit.HOURS);
        }
        
        return rules;
    }
    
    /**
     * 批量异步计算打卡结果
     * 
     * @param requests 打卡请求列表
     * @return 异步任务ID
     */
    public String batchCalculateAsync(List<ClockInRequest> requests) {
        String taskId = IdUtil.fastUUID();
        
        // 发送到消息队列异步处理
        BatchClockInMessage message = new BatchClockInMessage();
        message.setTaskId(taskId);
        message.setRequests(requests);
        
        rocketMQTemplate.asyncSend("BATCH_CLOCK_IN_TOPIC", message);
        
        return taskId;
    }
    
    /**
     * 构建规则缓存键
     */
    private String buildRuleCacheKey(String shiftId) {
        return "flexible:rule:shift:" + shiftId;
    }
}

5.2 监控与告警

打卡监控服务

/**
 * 打卡监控服务
 * 监控打卡业务的健康状态和异常情况
 */
@Component
@Slf4j
public class ClockInMonitoringService {
    
    @Autowired
    private MeterRegistry meterRegistry;
    
    @Autowired
    private NotificationService notificationService;
    
    /**
     * 记录打卡成功
     */
    public void recordClockInSuccess(String employeeId, String shiftId) {
        // 记录成功计数
        meterRegistry.counter("clock.in.success",
            "shift", shiftId
        ).increment();
        
        // 记录最近打卡时间
        String key = "clock.in.last:" + employeeId;
        redisTemplate.opsForValue().set(key, System.currentTimeMillis(), 24, TimeUnit.HOURS);
    }
    
    /**
     * 记录打卡失败
     */
    public void recordClockInFailure(String employeeId, String shiftId, String errorMessage) {
        // 记录失败计数
        meterRegistry.counter("clock.in.failure",
            "shift", shiftId,
            "error", categorizeError(errorMessage)
        ).increment();
        
        // 记录失败详情
        String key = "clock.in.fail:" + employeeId + ":" + System.currentTimeMillis();
        redisTemplate.opsForValue().set(key, errorMessage, 7, TimeUnit.DAYS);
    }
    
    /**
     * 检测打卡异常
     * 
     * @param employeeId 员工ID
     * @return 异常检测结果
     */
    public AnomalyDetectionResult detectClockInAnomaly(String employeeId) {
        AnomalyDetectionResult result = AnomalyDetectionResult.builder()
            .employeeId(employeeId)
            .build();
        
        // 1. 检查连续未打卡天数
        int consecutiveAbsentDays = getConsecutiveAbsentDays(employeeId);
        if (consecutiveAbsentDays >= 3) {
            result.setHasAnomaly(true);
            result.addAnomaly("连续" + consecutiveAbsentDays + "天未打卡");
        }
        
        // 2. 检查频繁迟到
        int frequentLateCount = getFrequentLateCount(employeeId, 30);
        if (frequentLateCount >= 10) {
            result.setHasAnomaly(true);
            result.addAnomaly("30天内迟到" + frequentLateCount + "次");
        }
        
        // 3. 检查打卡位置异常
        List<String> abnormalLocations = getAbnormalLocations(employeeId);
        if (!abnormalLocations.isEmpty()) {
            result.setHasAnomaly(true);
            result.addAnomaly("打卡位置异常: " + String.join(", ", abnormalLocations));
        }
        
        // 4. 发送告警
        if (result.getHasAnomaly()) {
            sendAnomalyAlert(employeeId, result.getAnomalies());
        }
        
        return result;
    }
    
    /**
     * 获取连续未打卡天数
     */
    private int getConsecutiveAbsentDays(String employeeId) {
        LocalDate today = LocalDate.now();
        int consecutiveDays = 0;
        
        for (int i = 1; i <= 30; i++) {
            LocalDate date = today.minusDays(i);
            AttendanceDailyDO record = attendanceDailyService.getByEmployeeAndDate(
                employeeId, date
            );
            
            if (record == null || "2".equals(record.getSignInStatus())) {
                consecutiveDays++;
            } else {
                break;
            }
        }
        
        return consecutiveDays;
    }
    
    /**
     * 获取频繁迟到次数
     */
    private int getFrequentLateCount(String employeeId, int days) {
        LocalDate startDate = LocalDate.now().minusDays(days);
        
        Long count = attendanceDailyMapper.selectCount(
            new LambdaQueryWrapper<AttendanceDailyDO>()
                .eq(AttendanceDailyDO::getEmployeeId, employeeId)
                .ge(AttendanceDailyDO::getAttendanceDate, startDate)
                .eq(AttendanceDailyDO::getSignInStatus, "1")
        );
        
        return count.intValue();
    }
    
    /**
     * 获取异常位置
     */
    private List<String> getAbnormalLocations(String employeeId) {
        // 分析打卡位置,找出异常位置
        // 这里简化处理
        return Collections.emptyList();
    }
    
    /**
     * 发送异常告警
     */
    private void sendAnomalyAlert(String employeeId, List<String> anomalies) {
        try {
            EmployeeDO employee = employeeMapper.selectById(employeeId);
            
            String message = String.format(
                "员工[%s]打卡异常:%s",
                employee.getName(),
                String.join(";", anomalies)
            );
            
            notificationService.sendAlertToManager(employee.getSuperiorId(), message);
            
        } catch (Exception e) {
            log.error("发送异常告警失败: employeeId={}, error={}", 
                employeeId, e.getMessage());
        }
    }
    
    /**
     * 错误分类
     */
    private String categorizeError(String errorMessage) {
        if (errorMessage.contains("位置")) {
            return "location";
        } else if (errorMessage.contains("时间")) {
            return "time";
        } else {
            return "other";
        }
    }
}

六、最佳实践

6.1 规则设计最佳实践

1. 规则优先级设置

// ✅ 推荐:合理的优先级设置
public class RulePriorityConfig {
    /**
     * 优先级规则:
     * - 异常宽限规则:优先级 100-199(最宽容,优先级最高)
     * - 灵活对调规则:优先级 200-299(保证工作时长)
     * - 标准规则:优先级 999(最严格,兜底规则)
     */
    
    public static final int GRACE_PERIOD_PRIORITY_MIN = 100;
    public static final int GRACE_PERIOD_PRIORITY_MAX = 199;
    
    public static final int FLEXIBLE_SWAP_PRIORITY_MIN = 200;
    public static final int FLEXIBLE_SWAP_PRIORITY_MAX = 299;
}

// ❌ 不推荐:混乱的优先级设置
// 优先级设置混乱,导致规则执行顺序不符合预期

2. 规则参数配置

// ✅ 推荐:合理的规则参数
FlexibleRuleDTO rule = FlexibleRuleDTO.builder()
    .ruleType(FlexibleRuleType.GRACE_PERIOD)
    .lateGraceMinutes(15)        // 15分钟迟到宽限
    .earlyLeaveGraceMinutes(15)   // 15分钟早退宽限
    .priority(100)                 // 优先级100
    .build();

// ❌ 不推荐:过大的宽限时间
FlexibleRuleDTO rule = FlexibleRuleDTO.builder()
    .ruleType(FlexibleRuleType.GRACE_PERIOD)
    .lateGraceMinutes(120)        // 2小时宽限太大
    .priority(100)
    .build();

6.2 性能优化最佳实践

1. 缓存策略

// ✅ 推荐:多级缓存策略
public class CacheStrategy {
    // L1缓存:本地缓存(Caffeine)
    // 缓存灵活规则,1小时过期
    private final Cache<String, List<AttendanceShiftFlexibleRuleDO>> localCache;
    
    // L2缓存:分布式缓存(Redis)
    // 缓存打卡结果,24小时过期
    
    // 缓存预热
    public void warmUpCache(List<String> shiftIds) {
        // 在打卡高峰前预热缓存
    }
}

// ❌ 不推荐:没有缓存
// 每次打卡都查询数据库,高峰期性能差

2. 批量处理

// ✅ 推荐:批量异步处理
public class BatchProcessing {
    // 使用消息队列批量处理打卡计算
    public void processBatch(List<ClockInRequest> requests) {
        rocketMQTemplate.asyncSend("BATCH_CLOCK_IN_TOPIC", requests);
    }
}

// ❌ 不推荐:同步串行处理
// 一个打卡请求阻塞后续请求

6.3 安全性最佳实践

1. 位置验证安全

// ✅ 推荐:严格的位置验证
public class LocationSecurity {
    // 1. 位置信息加密传输
    // 2. 验证位置来源(GPS vs WiFi)
    // 3. 检测位置模拟
    // 4. 记录位置历史用于分析
}

// ❌ 不推荐:宽松的位置验证
// 容易被绕过,存在打卡作弊风险

七、总结

本文详细介绍了考勤打卡灵活规则引擎的设计与实现,主要内容包括:

核心技术点

  1. 策略模式应用:灵活对调规则和异常宽限规则的策略实现
  2. 规则调度引擎:协调多个规则的执行顺序和结果聚合
  3. 位置验证:基于Haversine公式的距离计算和范围验证
  4. 规则冲突处理:解决多个规则同时适用的冲突问题
  5. 规则版本管理:支持规则的历史版本追溯和恢复
  6. 性能优化:缓存策略、批量处理、监控告警

业务价值

  1. 提升员工满意度:人性化的打卡制度改善工作体验
  2. 降低管理成本:减少异常打卡处理的审批流程
  3. 增强组织适应性:适应弹性工作制和远程办公
  4. 保证工作质量:通过规则设计确保工作时长
  5. 数据化决策:完整的打卡数据支持管理决策

扩展方向

  1. AI规则推荐:根据历史数据推荐最优规则配置
  2. 智能异常检测:使用机器学习检测打卡作弊
  3. 预测性分析:预测高峰时段,提前优化资源配置
  4. 移动端优化:优化移动端打卡体验
  5. 多场景支持:支持远程打卡、外勤打卡等场景
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐