tuqin
5 天以前 852b6547eb0cedcc6accf36214b0574d745f3208
feat(hrm): 从标品分支移植 APP 打卡状态接口 /hrm/attendance/record/my-status

- 新增 GET /hrm/attendance/record/my-status:返回当日考勤规则、打卡记录、工作日/休息原因、可打卡状态(clockIn/clockOut/nextAction/message)
- 新增 HrmAttendanceMyStatusRespVO
- HrmAttendanceClockTypeEnum 增加 getNameByType
- HrmAttendanceRecordRespVO 增加 clockInTypeName / clockOutTypeName
- 规则匹配适配本分支的 getAttendanceRuleByDeptId(deptId, date)(支持同部门多规则按生效日期)
- 修复 APP 打卡页因接口不存在(请求地址不存在)导致无法打卡

移植来源:origin/dev_business
已修改5个文件
已添加1个文件
273 ■■■■■ 文件已修改
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 14 ●●●●● 补丁 | 查看 | 原始文档 | 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 161 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/HrmAttendanceClockTypeEnum.java
@@ -23,6 +23,21 @@
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(HrmAttendanceClockTypeEnum::getType).toArray(Integer[]::new);
    /**
     * æ ¹æ®ç±»åž‹èŽ·å¾—åç§°ï¼ˆç”¨äºŽç•Œé¢å±•ç¤ºï¼Œå¦‚ my-status è¿”回)
     */
    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;
    }
    @Override
    public Integer[] array() {
        return ARRAYS;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceRecordController.java
@@ -11,6 +11,7 @@
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.controller.admin.attendance.vo.HrmAttendanceMyStatusRespVO;
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.service.attendance.HrmAttendanceRecordService;
@@ -30,6 +31,7 @@
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;
@@ -96,10 +98,18 @@
        return success(respVO);
    }
    private Long getLoginUserId() {
        return cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId();
    @GetMapping("/my-status")
    @Operation(summary = "获得当前登录人的考勤状态", description = "App æ‰“卡页使用:返回当日考勤规则、打卡记录与可打卡状态")
    @Parameter(name = "date", description = "考勤日期,格式 yyyy-MM-dd,默认服务器当天", example = "2026-10-17")
    public CommonResult<HrmAttendanceMyStatusRespVO> getMyStatus(
            @RequestParam(value = "date", required = false)
            @org.springframework.format.annotation.DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
        return success(attendanceRecordService.getMyStatus(getLoginUserId(), date));
    }
    private Long getLoginUserId() {
        return cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId();    }
    @GetMapping("/page")
    @Operation(summary = "获得考勤记录分页")
    @PreAuthorize("@ss.hasPermission('hrm:attendance-record:query')")
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
@@ -60,6 +60,12 @@
    @DictFormat("hrm_clock_type")
    private Integer clockOutType;
    @Schema(description = "上班打卡类型名称(正常/迟到/缺卡)")
    private String clockInTypeName;
    @Schema(description = "下班打卡类型名称(正常/早退/缺卡)")
    private String clockOutTypeName;
    @Schema(description = "工作时长(小时)")
    @ExcelProperty("工作时长")
    private BigDecimal workHours;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordService.java
@@ -70,4 +70,14 @@
     */
    void supplementClockIn(HrmAttendanceExceptionDO exception);
    /**
     * èŽ·å¾—å½“å‰ç™»å½•äººçš„è€ƒå‹¤çŠ¶æ€ï¼ˆApp æ‰“卡页使用)
     *
     * @param loginUserId ç™»å½•用户ID(系统用户,非员工ID)
     * @param date        è€ƒå‹¤æ—¥æœŸï¼Œä¸ºç©ºæ—¶å–服务器当天
     * @return æˆ‘的考勤状态
     */
    cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceMyStatusRespVO
    getMyStatus(Long loginUserId, LocalDate date);
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java
@@ -3,6 +3,13 @@
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.HrmAttendanceRecordPageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceMyStatusRespVO;
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.enums.HrmAttendanceHolidayTypeEnum;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
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;
@@ -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) {
@@ -303,4 +313,155 @@
        attendanceRecordMapper.updateById(record);
    }
    // ==================== æˆ‘的考勤状态(App æ‰“卡页) ====================
    @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(), queryDate);
        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());
        }
        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;
    }
}