gaoluyang
8 小时以前 e4432d49f9d74e69cc71ed1141ebcd30c817e998
feat(hrm): 新增我的考勤状态接口并开放全员打卡

- 新增 GET /hrm/attendance/record/my-status,返回登录人员工档案、工作日与休息原因、当前生效考勤规则、当日考勤记录及可打卡状态
- clock-in / clock-out / my-status 移除权限点校验,仅要求登录态,打卡改为全员可用
- 打卡成功返回补齐 clockInTypeName / clockOutTypeName,与 my-status.record 字段保持一致
- 考勤记录 Response VO 增加打卡类型中文名,枚举补充 getNameByType
- 未关联员工档案时返回可执行提示,引导管理员在员工管理中绑定账号

Co-Authored-By: Claude Code <noreply@anthropic.com>
已添加1个文件
已修改7个文件
311 ■■■■■ 文件已修改
docs/project-business/data/modules.json 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/data/roles.json 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/HrmAttendanceClockTypeEnum.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceRecordController.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceMyStatusRespVO.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRecordRespVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordService.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java 166 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/project-business/data/modules.json
@@ -83,7 +83,7 @@
    {"key": "hrm", "name": "人力资源", "color": "#9db85a", "tag": "HRM", "flow": null,
     "positioning": "员工入转调离与考勤薪酬",
     "problem": "人事流程审批化、离职数据不丢(交接)、算薪自动化",
     "features": ["员工档案(入职自动建系统账号)", "请假/加班/调岗/离职 BPM å®¡æ‰¹", "离职交接映射 expandUserIds", "考勤(打卡·规则·排班·补卡审批·月度汇总)", "薪酬(应发-社保公积金-个税-请假扣款·1.5/2/3倍加班费·发放台账)", "培训证书(SQL先行待实现)"],
     "features": ["员工档案(入职自动建系统账号)", "请假/加班/调岗/离职 BPM å®¡æ‰¹", "离职交接映射 expandUserIds", "考勤(移动端我的考勤状态·打卡·规则·排班·补卡审批·月度汇总)", "薪酬(应发-社保公积金-个税-请假扣款·1.5/2/3倍加班费·发放台账)", "培训证书(SQL先行待实现)"],
     "objects": ["员工", "请假单", "离职申请", "交接映射", "考勤记录", "薪酬核算"],
     "roles": ["HR", "部门主管", "员工本人"],
     "upstream": [], "downstream": ["各模块(数据可见性扩展)", "system(账号)"],
docs/project-business/data/roles.json
@@ -18,7 +18,7 @@
    {"role": "设备员/维修工", "modules": ["MES-DV"], "operations": ["点检保养", "维修工单", "工装领用"], "data": ["mes_dv_*", "mes_tm_*"], "scope": "chargeUserId å½’属"},
    {"role": "客服/售后工程师", "modules": ["售后"], "operations": ["工单受理派工", "ç»´ä¿®", "退货申请"], "data": ["after_sale_*"], "scope": "行级数据权限四视图:我负责/我参与/下属负责/全部(aftersales:view-all)"},
    {"role": "财务", "modules": ["ERP", "CRM"], "operations": ["收款单", "付款单", "开票审核", "结算账户"], "data": ["erp_finance_*", "crm_invoice", "crm_receivable"], "scope": "financeUserId å½’属"},
    {"role": "HR", "modules": ["HRM"], "operations": ["员工档案入转调离", "考勤规则排班", "薪酬核算发放", "离职交接"], "data": ["hrm_*"], "scope": "部门范围 dataScope"},
    {"role": "HR", "modules": ["HRM"], "operations": ["员工档案入转调离", "考勤规则排班(本人打卡全员可用,不设权限点)", "薪酬核算发放", "离职交接"], "data": ["hrm_*"], "scope": "部门范围 dataScope"},
    {"role": "管理层", "modules": ["BI"], "operations": ["查看采购销售/仓库/生产设备/质量/人力五大主题大屏"], "data": ["bi_dashboard"], "scope": "只读"}
  ]
}
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/HrmAttendanceClockTypeEnum.java
@@ -28,4 +28,19 @@
        return ARRAYS;
    }
    /**
     * æ ¹æ®ç±»åž‹èŽ·å–åç§°ï¼ŒæœªçŸ¥ç±»åž‹è¿”å›ž null
     */
    public static String getNameByType(Integer type) {
        if (type == null) {
            return null;
        }
        for (HrmAttendanceClockTypeEnum value : values()) {
            if (value.getType().equals(type)) {
                return value.getName();
            }
        }
        return null;
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceRecordController.java
@@ -8,12 +8,14 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.framework.operatelog.core.util.OperateLogUtils;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceMyStatusRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordImportExcelVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordImportReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordPageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordRespVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRecordDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.enums.HrmAttendanceClockTypeEnum;
import cn.iocoder.yudao.module.hrm.service.attendance.HrmAttendanceRecordService;
import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
@@ -27,11 +29,13 @@
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
@@ -39,6 +43,7 @@
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
import static cn.iocoder.yudao.module.hrm.enums.ErrorCodeConstants.EMPLOYEE_NOT_EXISTS;
import static cn.iocoder.yudao.module.hrm.enums.LogRecordConstants.*;
@@ -59,36 +64,46 @@
    @PostMapping("/clock-in")
    @Operation(summary = "上班打卡")
    @PreAuthorize("@ss.hasPermission('hrm:attendance-record:clock')")
    public CommonResult<HrmAttendanceRecordRespVO> clockIn(@RequestParam("location") String location) {
        Long loginUserId = getLoginUserId();
        HrmEmployeeDO employee = employeeService.getEmployeeByUserId(loginUserId);
        if (employee == null) {
            throw exception(EMPLOYEE_NOT_EXISTS, "当前用户未关联员工信息,无法打卡");
            throw exception(EMPLOYEE_NOT_EXISTS,
                    "当前登录账号未关联员工档案,无法打卡。请联系管理员在【员工管理】中将该登录账号绑定到员工。");
        }
        HrmAttendanceRecordDO record = attendanceRecordService.clockIn(employee.getId(), LocalDateTime.now(), location);
        HrmAttendanceRecordRespVO respVO = BeanUtils.toBean(record, HrmAttendanceRecordRespVO.class);
        respVO.setUserName(employee.getName());
        if (record.getDeptId() != null) {
            DeptRespDTO dept = deptApi.getDept(record.getDeptId());
            if (dept != null) {
                respVO.setDeptName(dept.getName());
            }
        }
        return success(respVO);
        return success(buildClockRespVO(record, employee));
    }
    @PostMapping("/clock-out")
    @Operation(summary = "下班打卡")
    @PreAuthorize("@ss.hasPermission('hrm:attendance-record:clock')")
    public CommonResult<HrmAttendanceRecordRespVO> clockOut(@RequestParam("location") String location) {
        Long loginUserId = getLoginUserId();
        HrmEmployeeDO employee = employeeService.getEmployeeByUserId(loginUserId);
        if (employee == null) {
            throw exception(EMPLOYEE_NOT_EXISTS, "当前用户未关联员工信息,无法打卡");
            throw exception(EMPLOYEE_NOT_EXISTS,
                    "当前登录账号未关联员工档案,无法打卡。请联系管理员在【员工管理】中将该登录账号绑定到员工。");
        }
        HrmAttendanceRecordDO record = attendanceRecordService.clockOut(employee.getId(), LocalDateTime.now(), location);
        return success(buildClockRespVO(record, employee));
    }
    @GetMapping("/my-status")
    @Operation(summary = "获得当前登录人的考勤状态")
    @Parameter(name = "date", description = "考勤日期,格式 yyyy-MM-dd,默认服务器当天", example = "2026-10-17")
    public CommonResult<HrmAttendanceMyStatusRespVO> getMyStatus(
            @RequestParam(value = "date", required = false)
            @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY) LocalDate date) {
        return success(attendanceRecordService.getMyStatus(getLoginUserId(), date));
    }
    /**
     * æž„建打卡返回 VO,字段与 my-status.record ä¿æŒä¸€è‡´
     */
    private HrmAttendanceRecordRespVO buildClockRespVO(HrmAttendanceRecordDO record, HrmEmployeeDO employee) {
        HrmAttendanceRecordRespVO respVO = BeanUtils.toBean(record, HrmAttendanceRecordRespVO.class);
        respVO.setClockInTypeName(HrmAttendanceClockTypeEnum.getNameByType(record.getClockInType()));
        respVO.setClockOutTypeName(HrmAttendanceClockTypeEnum.getNameByType(record.getClockOutType()));
        respVO.setUserName(employee.getName());
        if (record.getDeptId() != null) {
            DeptRespDTO dept = deptApi.getDept(record.getDeptId());
@@ -96,7 +111,7 @@
                respVO.setDeptName(dept.getName());
            }
        }
        return success(respVO);
        return respVO;
    }
    private Long getLoginUserId() {
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceMyStatusRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,67 @@
package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - æˆ‘的考勤状态 Response VO")
@Data
public class HrmAttendanceMyStatusRespVO {
    @Schema(description = "登录用户ID", example = "464")
    private Long userId;
    @Schema(description = "员工档案ID", example = "233")
    private Long employeeId;
    @Schema(description = "员工姓名", example = "阿卜杜克热木·吾拉依木")
    private String employeeName;
    @Schema(description = "部门ID", example = "131")
    private Long deptId;
    @Schema(description = "部门名称", example = "钢结构车间")
    private String deptName;
    @Schema(description = "考勤日期", example = "2026-10-17")
    private LocalDate date;
    @Schema(description = "服务器时间", example = "2026-10-17 18:30:00")
    private LocalDateTime serverTime;
    @Schema(description = "是否工作日")
    private Boolean workDay;
    @Schema(description = "休息日原因,如:国庆节、周六")
    private String restDayReason;
    @Schema(description = "当前生效的考勤规则,未配置时为 null")
    private HrmAttendanceRuleRespVO rule;
    @Schema(description = "当日考勤记录,未打卡时为 null")
    private HrmAttendanceRecordRespVO record;
    @Schema(description = "当前可打卡状态")
    private Clockable clockable;
    @Schema(description = "打卡状态")
    @Data
    public static class Clockable {
        @Schema(description = "是否可上班打卡")
        private Boolean clockIn;
        @Schema(description = "是否可下班打卡")
        private Boolean clockOut;
        @Schema(description = "下一步动作:in-上班打卡,out-下班打卡,none-无需打卡")
        private String nextAction;
        @Schema(description = "提示文案", example = "已上班打卡,可下班打卡")
        private String message;
    }
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRecordRespVO.java
@@ -55,11 +55,17 @@
    @DictFormat("hrm_clock_type")
    private Integer clockInType;
    @Schema(description = "上班打卡类型名称", example = "正常")
    private String clockInTypeName;
    @Schema(description = "下班打卡类型")
    @ExcelProperty(value = "下班打卡类型", converter = DictConvert.class)
    @DictFormat("hrm_clock_type")
    private Integer clockOutType;
    @Schema(description = "下班打卡类型名称", example = "早退")
    private String clockOutTypeName;
    @Schema(description = "工作时长(小时)")
    @ExcelProperty("工作时长")
    private BigDecimal workHours;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordService.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.hrm.service.attendance;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceMyStatusRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordPageReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRecordDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceExceptionDO;
@@ -37,6 +38,15 @@
     */
    HrmAttendanceRecordDO getAttendanceRecordByUserIdAndDate(Long userId, LocalDate date);
    /**
     * èŽ·å¾—å½“å‰ç™»å½•äººçš„è€ƒå‹¤çŠ¶æ€ï¼šè€ƒå‹¤è§„åˆ™ + æŒ‡å®šæ—¥æœŸçš„æ‰“卡状态
     *
     * @param loginUserId ç™»å½•用户ID(系统用户ID,不是员工ID)
     * @param date        è€ƒå‹¤æ—¥æœŸï¼Œä¸ºç©ºæ—¶å–服务器当天
     * @return æˆ‘的考勤状态
     */
    HrmAttendanceMyStatusRespVO getMyStatus(Long loginUserId, LocalDate date);
    /** åˆ›å»ºå¯¼å…¥çš„考勤记录 */
    void createImportedRecord(HrmAttendanceRecordDO record);
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java
@@ -2,7 +2,11 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceMyStatusRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordPageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordRespVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRuleRespVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceHolidayDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRecordDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRuleDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceExceptionDO;
@@ -11,7 +15,10 @@
import cn.iocoder.yudao.module.hrm.enums.HrmAttendanceClockTypeEnum;
import cn.iocoder.yudao.module.hrm.enums.HrmAttendanceDataSourceEnum;
import cn.iocoder.yudao.module.hrm.enums.HrmAttendanceExceptionTypeEnum;
import cn.iocoder.yudao.module.hrm.enums.HrmAttendanceHolidayTypeEnum;
import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -19,6 +26,7 @@
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.DayOfWeek;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
@@ -45,6 +53,8 @@
    private HrmAttendanceRuleService attendanceRuleService;
    @Resource
    private HrmAttendanceHolidayService attendanceHolidayService;
    @Resource
    private DeptApi deptApi;
    @Override
    public PageResult<HrmAttendanceRecordDO> getAttendanceRecordPage(HrmAttendanceRecordPageReqVO pageReqVO) {
@@ -67,6 +77,162 @@
    }
    @Override
    public HrmAttendanceMyStatusRespVO getMyStatus(Long loginUserId, LocalDate date) {
        LocalDate queryDate = date != null ? date : LocalDate.now();
        // 1. ç™»å½•用户 -> å‘˜å·¥æ¡£æ¡ˆ
        HrmEmployeeDO employee = employeeService.getEmployeeByUserId(loginUserId);
        if (employee == null) {
            throw exception(EMPLOYEE_NOT_EXISTS,
                    "当前登录账号未关联员工档案,无法打卡。请联系管理员在【员工管理】中将该登录账号绑定到员工。");
        }
        HrmAttendanceMyStatusRespVO respVO = new HrmAttendanceMyStatusRespVO();
        respVO.setUserId(loginUserId);
        respVO.setEmployeeId(employee.getId());
        respVO.setEmployeeName(employee.getName());
        respVO.setDeptId(employee.getDeptId());
        if (employee.getDeptId() != null) {
            DeptRespDTO dept = deptApi.getDept(employee.getDeptId());
            if (dept != null) {
                respVO.setDeptName(dept.getName());
            }
        }
        respVO.setDate(queryDate);
        respVO.setServerTime(LocalDateTime.now());
        // 2. æ˜¯å¦å·¥ä½œæ—¥ + ä¼‘息原因
        boolean workDay = attendanceHolidayService.isWorkDay(queryDate);
        respVO.setWorkDay(workDay);
        if (!workDay) {
            respVO.setRestDayReason(getRestDayReason(queryDate));
        }
        // 3. å½“前生效规则(部门规则优先,其次全局规则)
        HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId());
        if (rule != null) {
            HrmAttendanceRuleRespVO ruleRespVO = BeanUtils.toBean(rule, HrmAttendanceRuleRespVO.class);
            if (rule.getDeptId() != null) {
                DeptRespDTO ruleDept = deptApi.getDept(rule.getDeptId());
                if (ruleDept != null) {
                    ruleRespVO.setDeptName(ruleDept.getName());
                }
            }
            respVO.setRule(ruleRespVO);
        }
        // 4. å½“日考勤记录
        HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(employee.getId(), queryDate);
        if (record != null) {
            respVO.setRecord(buildRecordRespVO(record, employee));
        }
        // 5. å½“前可打卡状态
        respVO.setClockable(buildClockable(workDay, rule, record, queryDate));
        return respVO;
    }
    /**
     * ä¼‘息日原因:优先取节假日配置名称,未配置时按星期给出“周六/周日”
     */
    private String getRestDayReason(LocalDate date) {
        List<HrmAttendanceHolidayDO> holidays = attendanceHolidayService.getAttendanceHolidayListByDateRange(date, date);
        for (HrmAttendanceHolidayDO holiday : holidays) {
            if (HrmAttendanceHolidayTypeEnum.REST_DAY.getType().equals(holiday.getType())) {
                return holiday.getName();
            }
        }
        DayOfWeek dayOfWeek = date.getDayOfWeek();
        if (dayOfWeek == DayOfWeek.SATURDAY) {
            return "周六";
        }
        if (dayOfWeek == DayOfWeek.SUNDAY) {
            return "周日";
        }
        return null;
    }
    /**
     * æž„建打卡记录返回 VO,补齐打卡类型名称、员工与部门信息
     */
    private HrmAttendanceRecordRespVO buildRecordRespVO(HrmAttendanceRecordDO record, HrmEmployeeDO employee) {
        HrmAttendanceRecordRespVO recordRespVO = BeanUtils.toBean(record, HrmAttendanceRecordRespVO.class);
        recordRespVO.setClockInTypeName(HrmAttendanceClockTypeEnum.getNameByType(record.getClockInType()));
        recordRespVO.setClockOutTypeName(HrmAttendanceClockTypeEnum.getNameByType(record.getClockOutType()));
        if (employee != null) {
            recordRespVO.setUserName(employee.getName());
            recordRespVO.setPhone(employee.getPhone());
        }
        if (record.getDeptId() != null) {
            DeptRespDTO dept = deptApi.getDept(record.getDeptId());
            if (dept != null) {
                recordRespVO.setDeptName(dept.getName());
            }
        }
        return recordRespVO;
    }
    /**
     * æ ¹æ®å·¥ä½œæ—¥ã€è€ƒå‹¤è§„则与当日记录计算可打卡状态
     */
    private HrmAttendanceMyStatusRespVO.Clockable buildClockable(boolean workDay, HrmAttendanceRuleDO rule,
                                                                 HrmAttendanceRecordDO record, LocalDate date) {
        HrmAttendanceMyStatusRespVO.Clockable clockable = new HrmAttendanceMyStatusRespVO.Clockable();
        if (!workDay) {
            clockable.setClockIn(false);
            clockable.setClockOut(false);
            clockable.setNextAction("none");
            clockable.setMessage("今日休息,无需打卡");
            return clockable;
        }
        if (rule == null) {
            clockable.setClockIn(false);
            clockable.setClockOut(false);
            clockable.setNextAction("none");
            clockable.setMessage("未配置考勤规则,请联系管理员");
            return clockable;
        }
        // åªæœ‰ä¸‹ç­å¡ã€æ²¡æœ‰ä¸Šç­å¡ï¼šå½“日已结束,不再允许继续打卡
        if (record != null && record.getClockInTime() == null && record.getClockOutTime() != null) {
            clockable.setClockIn(false);
            clockable.setClockOut(false);
            clockable.setNextAction("none");
            clockable.setMessage("今日缺上班卡,已完成下班打卡");
            return clockable;
        }
        // ä»Šå¤©è¿˜æ²¡æ‰“上班卡
        if (record == null || record.getClockInTime() == null) {
            clockable.setClockIn(true);
            clockable.setClockOut(false);
            clockable.setNextAction("in");
            clockable.setMessage("可上班打卡");
            // ä»…当天查询时提示已迟到时长
            LocalDateTime now = LocalDateTime.now();
            if (rule.getWorkStartTime() != null && date.equals(now.toLocalDate())) {
                LocalDateTime workStart = LocalDateTime.of(date, rule.getWorkStartTime());
                int lateAllowMinutes = rule.getLateAllowMinutes() != null ? rule.getLateAllowMinutes() : 0;
                if (now.isAfter(workStart.plusMinutes(lateAllowMinutes))) {
                    long lateMinutes = Duration.between(workStart, now).toMinutes();
                    clockable.setMessage("已超过上班时间 " + lateMinutes + " åˆ†é’Ÿ");
                }
            }
            return clockable;
        }
        // å·²æ‰“上班卡、未打下班卡
        if (record.getClockOutTime() == null) {
            clockable.setClockIn(false);
            clockable.setClockOut(true);
            clockable.setNextAction("out");
            clockable.setMessage("已上班打卡,可下班打卡");
            return clockable;
        }
        // ä¸Šä¸‹ç­å¡éƒ½å·²æ‰“
        clockable.setClockIn(false);
        clockable.setClockOut(false);
        clockable.setNextAction("none");
        clockable.setMessage("今日已完成打卡");
        return clockable;
    }
    @Override
    public void createImportedRecord(HrmAttendanceRecordDO record) {
        // æŒ‰è€ƒå‹¤è§„则重算打卡类型与时长,不信任 Excel å¡«å†™çš„值
        fillClockTypes(record);