From e4432d49f9d74e69cc71ed1141ebcd30c817e998 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期六, 17 十月 2026 20:34:24 +0800
Subject: [PATCH] feat(hrm): 新增我的考勤状态接口并开放全员打卡

---
 yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java |  246 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 246 insertions(+), 0 deletions(-)

diff --git a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java b/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java
index f7ff44c..92f745d 100644
--- a/yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java
+++ b/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,242 @@
     }
 
     @Override
+    public HrmAttendanceMyStatusRespVO getMyStatus(Long loginUserId, LocalDate date) {
+        LocalDate queryDate = date != null ? date : LocalDate.now();
+        // 1. 鐧诲綍鐢ㄦ埛 -> 鍛樺伐妗f
+        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);
+        fillCalculatedHours(record);
+        attendanceRecordMapper.insert(record);
+    }
+
+    @Override
+    public void updateImportedRecord(HrmAttendanceRecordDO record) {
+        fillClockTypes(record);
+        fillCalculatedHours(record);
+        attendanceRecordMapper.updateById(record);
+    }
+
+    /**
+     * 鎸夎�冨嫟瑙勫垯閲嶇畻涓婁笅鐝墦鍗$被鍨嬶紙姝e父/杩熷埌/鏃╅��锛夛紝涓庢墦鍗¢�昏緫淇濇寔涓�鑷�
+     */
+    private void fillClockTypes(HrmAttendanceRecordDO record) {
+        if (record.getDeptId() == null) {
+            return;
+        }
+        HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(record.getDeptId());
+        if (rule == null) {
+            return;
+        }
+        // 涓婄彮鎵撳崱绫诲瀷锛氭櫄浜� 涓婄彮鏃堕棿+杩熷埌鍏佽 璁颁负杩熷埌
+        if (record.getClockInTime() != null && rule.getWorkStartTime() != null) {
+            int lateAllowMinutes = rule.getLateAllowMinutes() != null ? rule.getLateAllowMinutes() : 0;
+            LocalTime lateThreshold = rule.getWorkStartTime().plusMinutes(lateAllowMinutes);
+            LocalTime clockInOnly = record.getClockInTime().toLocalTime();
+            record.setClockInType(clockInOnly.isAfter(lateThreshold)
+                    ? HrmAttendanceClockTypeEnum.LATE.getType()
+                    : HrmAttendanceClockTypeEnum.NORMAL.getType());
+        }
+        // 涓嬬彮鎵撳崱绫诲瀷锛氭棭浜� 涓嬬彮鏃堕棿-鏃╅��鍏佽 璁颁负鏃╅��
+        if (record.getClockOutTime() != null && rule.getWorkEndTime() != null) {
+            int earlyAllowMinutes = rule.getEarlyAllowMinutes() != null ? rule.getEarlyAllowMinutes() : 0;
+            LocalTime earlyThreshold = rule.getWorkEndTime().minusMinutes(earlyAllowMinutes);
+            LocalTime clockOutOnly = record.getClockOutTime().toLocalTime();
+            record.setClockOutType(clockOutOnly.isBefore(earlyThreshold)
+                    ? HrmAttendanceClockTypeEnum.EARLY.getType()
+                    : HrmAttendanceClockTypeEnum.NORMAL.getType());
+        }
+    }
+
+    /**
+     * 鏍规嵁涓婁笅鐝墦鍗℃椂闂村拰鑰冨嫟瑙勫垯锛岃绠楀伐浣滄椂闀夸笌鍔犵彮鏃堕暱
+     */
+    private void fillCalculatedHours(HrmAttendanceRecordDO record) {
+        // 宸ヤ綔鏃堕暱 = 涓嬬彮鎵撳崱鏃堕棿 - 涓婄彮鎵撳崱鏃堕棿锛堜笌鎵撳崱閫昏緫涓�鑷达級
+        if (record.getClockInTime() != null && record.getClockOutTime() != null) {
+            Duration duration = Duration.between(record.getClockInTime(), record.getClockOutTime());
+            BigDecimal workHours = BigDecimal.valueOf(duration.toMinutes())
+                    .divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP);
+            record.setWorkHours(workHours);
+        }
+        // 鍔犵彮鏃堕暱锛氭寜鑰冨嫟瑙勫垯鐨勬爣鍑嗕笅鐝椂闂磋绠楄秴棰濋儴鍒�
+        if (record.getClockOutTime() != null && record.getDeptId() != null) {
+            HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(record.getDeptId());
+            if (rule != null && rule.getWorkEndTime() != null) {
+                LocalTime workEnd = rule.getWorkEndTime();
+                LocalDate outDate = record.getClockOutTime().toLocalDate();
+                LocalDateTime workEndDateTime = LocalDateTime.of(outDate, workEnd);
+                if (record.getClockOutTime().isAfter(workEndDateTime)) {
+                    Duration overtimeDuration = Duration.between(workEndDateTime, record.getClockOutTime());
+                    BigDecimal overtimeThreshold = rule.getOvertimeThreshold() != null
+                            ? rule.getOvertimeThreshold() : BigDecimal.ZERO;
+                    BigDecimal overtimeHours = BigDecimal.valueOf(overtimeDuration.toMinutes())
+                            .divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP);
+                    record.setOvertimeHours(overtimeHours.compareTo(overtimeThreshold) > 0 ? overtimeHours : BigDecimal.ZERO);
+                } else {
+                    record.setOvertimeHours(BigDecimal.ZERO);
+                }
+            } else {
+                record.setOvertimeHours(BigDecimal.ZERO);
+            }
+        }
+    }
+
+    @Override
     @Transactional(rollbackFor = Exception.class)
     public HrmAttendanceRecordDO clockIn(Long userId, LocalDateTime clockTime, String location) {
         LocalDate clockDate = clockTime.toLocalDate();

--
Gitblit v1.9.3