From 82155dd6f2b16fbfb718e636a54e2c9dae31246b Mon Sep 17 00:00:00 2001
From: liu <2021943741@qq.com>
Date: 星期五, 30 十月 2026 20:23:20 +0800
Subject: [PATCH] feat(hrm,bi): 移植长辉电力的考勤请假联动与 BI 大屏图表
---
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java | 561 +++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 files changed, 532 insertions(+), 29 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..cd53325 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
@@ -1,8 +1,14 @@
package cn.iocoder.yudao.module.hrm.service.attendance;
+import cn.hutool.core.util.StrUtil;
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.HrmAttendanceClockReqVO;
+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 +17,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 +28,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 +55,10 @@
private HrmAttendanceRuleService attendanceRuleService;
@Resource
private HrmAttendanceHolidayService attendanceHolidayService;
+ @Resource
+ private DeptApi deptApi;
+ @Resource
+ private cn.iocoder.yudao.module.hrm.service.approval.HrmLeaveQueryService leaveQueryService;
@Override
public PageResult<HrmAttendanceRecordDO> getAttendanceRecordPage(HrmAttendanceRecordPageReqVO pageReqVO) {
@@ -67,61 +81,441 @@
}
@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(ATTENDANCE_EMPLOYEE_NOT_BOUND);
+ }
+ HrmAttendanceMyStatusRespVO respVO = new HrmAttendanceMyStatusRespVO();
+ respVO.setUserId(loginUserId);
+ respVO.setEmployeeId(employee.getId());
+ respVO.setEmployeeName(employee.getName());
+ respVO.setUserName(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);
+ fillCompatibleRuleFields(respVO, rule);
+ }
+
+ // 4. 褰撴棩鑰冨嫟璁板綍
+ // 鑰冨嫟璁板綍琛ㄥ瓨鐨勬槸绯荤粺鐢ㄦ埛 ID锛岃繖閲岀敤 loginUserId 鑰屼笉鏄憳宸ユ。妗� ID
+ HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(loginUserId, queryDate);
+ if (record != null) {
+ respVO.setRecord(buildRecordRespVO(record, employee));
+ fillCompatibleRecordFields(respVO, record);
+ }
+
+ // 4.5 褰撳ぉ璇峰亣鎯呭喌锛堣鍋囦粛鍏佽鎵撳崱锛屼絾涓嶇畻杩熷埌/鏃╅��/缂哄崱锛�
+ List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO> leaves =
+ leaveQueryService.getApprovedLeaveList(loginUserId, queryDate);
+ boolean onLeave = !leaves.isEmpty();
+ java.math.BigDecimal leaveHours = onLeave ? calcLeaveHours(leaves, rule, queryDate) : java.math.BigDecimal.ZERO;
+ respVO.setOnLeave(onLeave);
+ respVO.setLeaveHours(leaveHours);
+ if (onLeave) {
+ List<String> typeNames = leaves.stream()
+ .map(l -> cn.iocoder.yudao.module.hrm.enums.HrmLeaveTypeEnum.getNameByStatus(l.getLeaveType()))
+ .distinct().toList();
+ respVO.setLeaveTypeNames(String.join("銆�", typeNames));
+ }
+
+ // 5. 褰撳墠鍙墦鍗$姸鎬�
+ HrmAttendanceMyStatusRespVO.Clockable clockable = buildClockable(workDay, rule, record, queryDate, onLeave);
+
+ // 5.1 璇峰亣褰撳ぉ锛氭妸銆屽彲鎵撳崱銆嶇殑鎻愮ず鎹㈡垚璇峰亣璇存槑锛屼絾渚濈劧鍏佽鎵撳崱
+ if (onLeave) {
+ clockable.setMessage("浠婃棩宸茶鍋囷紙" + respVO.getLeaveTypeNames() + "锛夛紝濡傞渶涓婄彮鍙甯告墦鍗�");
+ }
+ respVO.setClockable(clockable);
+ respVO.setClockInDone(record != null && record.getClockInTime() != null);
+ respVO.setClockOutDone(record != null && record.getClockOutTime() != null);
+ respVO.setCanClockIn(clockable.getClockIn());
+ respVO.setCanClockOut(clockable.getClockOut());
+ return respVO;
+ }
+
+ /** 鍚屾椂淇濈暀鏃х増 App/Web 浣跨敤鐨勫钩閾鸿鍒欏瓧娈点�� */
+ private void fillCompatibleRuleFields(HrmAttendanceMyStatusRespVO respVO, HrmAttendanceRuleDO rule) {
+ respVO.setRuleName(rule.getName());
+ respVO.setWorkStartTime(rule.getWorkStartTime());
+ respVO.setWorkEndTime(rule.getWorkEndTime());
+ respVO.setLocationRequired(rule.getLocationRequired());
+ respVO.setLocationName(rule.getLocationName());
+ respVO.setLocationAddress(rule.getLocationAddress());
+ respVO.setLongitude(rule.getLongitude());
+ respVO.setLatitude(rule.getLatitude());
+ respVO.setRadiusMeters(rule.getRadiusMeters());
+ respVO.setCoordinateSystem(rule.getCoordinateSystem());
+ }
+
+ /** 鍚屾椂淇濈暀鏃х増 App/Web 浣跨敤鐨勫钩閾鸿�冨嫟璁板綍瀛楁銆� */
+ private void fillCompatibleRecordFields(HrmAttendanceMyStatusRespVO respVO, HrmAttendanceRecordDO record) {
+ respVO.setRecordId(record.getId());
+ respVO.setClockInTime(record.getClockInTime());
+ respVO.setClockOutTime(record.getClockOutTime());
+ respVO.setClockInType(record.getClockInType());
+ respVO.setClockOutType(record.getClockOutType());
+ respVO.setWorkHours(record.getWorkHours());
+ respVO.setOvertimeHours(record.getOvertimeHours());
+ respVO.setLocation(record.getLocation());
+ respVO.setDataSource(record.getDataSource());
+ }
+
+ /**
+ * 鎶婅鍋囦俊鎭啓鍏ヨ�冨嫟璁板綍锛坙eave_flag / leave_hours / leave_types锛�
+ *
+ * @param record 鑰冨嫟璁板綍
+ * @param leaves 褰撳ぉ瀹℃壒閫氳繃鐨勮鍋囧崟
+ * @param rule 鑰冨嫟瑙勫垯锛堢敤浜庤绠楀伐浣滄椂娈典氦闆嗭級
+ * @param date 鑰冨嫟鏃ユ湡
+ */
+ private void applyLeaveInfo(HrmAttendanceRecordDO record,
+ List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO> leaves,
+ HrmAttendanceRuleDO rule, LocalDate date) {
+ if (leaves == null || leaves.isEmpty()) {
+ record.setLeaveFlag(false);
+ record.setLeaveHours(BigDecimal.ZERO);
+ record.setLeaveTypes(null);
+ return;
+ }
+ record.setLeaveFlag(true);
+ record.setLeaveHours(calcLeaveHours(leaves, rule, date));
+ record.setLeaveTypes(leaves.stream()
+ .map(l -> String.valueOf(l.getLeaveType()))
+ .distinct()
+ .collect(java.util.stream.Collectors.joining(",")));
+ }
+
+ /**
+ * 璁$畻鏌愬ぉ鐨勮鍋囧伐鏃�
+ *
+ * <p>鎶婃瘡寮犺鍋囧崟鐨� [start_time, end_time] 涓庤�冨嫟瑙勫垯鐨勫伐浣滄椂娈垫眰浜ら泦鍚庣疮鍔狅紝
+ * 绮剧‘鍒� 0.01 灏忔椂锛�36 绉掞級銆傛棤瑙勫垯鏃舵寜鏁村ぉ鐨勮鍋囨椂闀挎姌绠椼��</p>
+ *
+ * <p>渚嬶細瑙勫垯 08:30-17:30锛岃鍋囧崟 14:00-18:00 鈫� 浜ら泦 14:00-17:30 = 3.5 灏忔椂銆�</p>
+ */
+ private java.math.BigDecimal calcLeaveHours(
+ List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO> leaves,
+ HrmAttendanceRuleDO rule, LocalDate date) {
+ LocalDateTime dayStart = date.atStartOfDay();
+ LocalDateTime dayEnd = date.plusDays(1).atStartOfDay();
+ // 宸ヤ綔鏃ユ椂娈碉細浼樺厛鐢ㄨ�冨嫟瑙勫垯锛屾病閰嶈鍒欏氨鎸夋暣澶�
+ LocalDateTime workStart;
+ LocalDateTime workEnd;
+ if (rule != null && rule.getWorkStartTime() != null && rule.getWorkEndTime() != null) {
+ workStart = LocalDateTime.of(date, rule.getWorkStartTime());
+ workEnd = LocalDateTime.of(date, rule.getWorkEndTime());
+ // 璺ㄩ浂鐐圭彮娆★紙濡� 20:00-娆℃棩 04:00锛�
+ if (!workEnd.isAfter(workStart)) {
+ workEnd = workEnd.plusDays(1);
+ }
+ } else {
+ workStart = dayStart;
+ workEnd = dayEnd;
+ }
+ java.math.BigDecimal total = java.math.BigDecimal.ZERO;
+ for (var leave : leaves) {
+ if (leave.getStartTime() == null || leave.getEndTime() == null) {
+ continue;
+ }
+ LocalDateTime from = leave.getStartTime().isAfter(workStart) ? leave.getStartTime() : workStart;
+ LocalDateTime to = leave.getEndTime().isBefore(workEnd) ? leave.getEndTime() : workEnd;
+ if (!to.isAfter(from)) {
+ continue;
+ }
+ long minutes = java.time.Duration.between(from, to).toMinutes();
+ total = total.add(java.math.BigDecimal.valueOf(minutes)
+ .divide(java.math.BigDecimal.valueOf(60), 2, java.math.RoundingMode.HALF_UP));
+ }
+ return total;
+ }
+
+ /**
+ * 浼戞伅鏃ュ師鍥狅細浼樺厛鍙栬妭鍋囨棩閰嶇疆鍚嶇О锛屾湭閰嶇疆鏃舵寜鏄熸湡缁欏嚭鈥滃懆鍏�/鍛ㄦ棩鈥�
+ */
+ 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,
+ boolean onLeave) {
+ 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 (!onLeave && 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) {
+ public HrmAttendanceClockResult clockIn(Long userId, LocalDateTime clockTime,
+ HrmAttendanceClockReqVO reqVO, String legacyLocation) {
LocalDate clockDate = clockTime.toLocalDate();
// 1. 鏍¢獙鏄惁宸ヤ綔鏃�
if (!attendanceHolidayService.isWorkDay(clockDate)) {
throw exception(ATTENDANCE_CLOCK_FAIL_NOT_WORKDAY);
}
// 2. 鑾峰彇鍛樺伐淇℃伅鍜岃�冨嫟瑙勫垯
+ // 娉ㄦ剰锛氬叆鍙� userId 瀹為檯浼犵殑鏄�屽憳宸ユ。妗� ID銆嶏紝
+ // 鑰岃�冨嫟璁板綍琛� / 璇峰亣琛ㄧ殑 user_id 瀛樼殑鏄�岀郴缁熺敤鎴� ID銆嶏紝蹇呴』鍖哄垎銆�
HrmEmployeeDO employee = employeeService.getEmployee(userId);
if (employee == null) {
throw exception(EMPLOYEE_NOT_EXISTS);
}
+ Long systemUserId = employee.getUserId();
HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId());
if (rule == null) {
throw exception(ATTENDANCE_CLOCK_FAIL_NO_RULE);
}
- // 3. 鏌ヨ褰撴棩鑰冨嫟璁板綍
- HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(userId, clockDate);
+ // 3. 鏌ヨ褰撴棩鑰冨嫟璁板綍锛堢敤绯荤粺鐢ㄦ埛 ID锛�
+ HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(systemUserId, clockDate);
if (record != null && record.getClockInTime() != null) {
throw exception(ATTENDANCE_CLOCK_ALREADY_CLOCKED);
}
+ // 鍦ㄤ换浣曞啓搴撴搷浣滃墠瀹屾垚瀹氫綅鏍¢獙锛屾棫鐗堝湴鐐瑰瓧绗︿覆涓嶈兘缁曡繃鑼冨洿闄愬埗銆�
+ ClockLocation clockLocation = validateClockLocation(rule, reqVO);
+ // 3.1 褰撳ぉ璇峰亣鎯呭喌锛氳鍋囧綋澶╁厑璁告墦鍗★紝浣嗕笉鍒ゅ畾涓鸿繜鍒�
+ // 娉ㄦ剰锛氭柟娉曞叆鍙� userId 瀹炰负銆屽憳宸ユ。妗� ID銆嶏紝鑰岃鍋囪〃瀛樼殑鏄�岀郴缁熺敤鎴� ID銆嶏紝
+ // 蹇呴』鐢� employee.getUserId() 鎵嶈兘瀵逛笂銆�
+ List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO> leaves =
+ leaveQueryService.getApprovedLeaveList(employee.getUserId(), clockDate);
+ boolean onLeave = !leaves.isEmpty();
// 4. 鍒ゆ柇鎵撳崱绫诲瀷
LocalTime workStartTime = rule.getWorkStartTime();
int lateAllowMinutes = rule.getLateAllowMinutes() != null ? rule.getLateAllowMinutes() : 0;
LocalTime lateThreshold = workStartTime.plusMinutes(lateAllowMinutes);
LocalTime clockTimeOnly = clockTime.toLocalTime();
- Integer clockInType = clockTimeOnly.isBefore(lateThreshold) || clockTimeOnly.equals(lateThreshold)
- ? HrmAttendanceClockTypeEnum.NORMAL.getType()
- : HrmAttendanceClockTypeEnum.LATE.getType();
+ Integer clockInType;
+ if (onLeave) {
+ // 璇峰亣褰撳ぉ锛氬彧瑕佹湁鎵撳崱灏辩畻姝e父锛岃繜鍒颁笌鍚︿笉褰卞搷鑰冨嫟
+ clockInType = HrmAttendanceClockTypeEnum.NORMAL.getType();
+ } else {
+ clockInType = clockTimeOnly.isBefore(lateThreshold) || clockTimeOnly.equals(lateThreshold)
+ ? HrmAttendanceClockTypeEnum.NORMAL.getType()
+ : HrmAttendanceClockTypeEnum.LATE.getType();
+ }
// 5. 鍒涘缓鎴栨洿鏂拌�冨嫟璁板綍
if (record == null) {
record = HrmAttendanceRecordDO.builder()
- .userId(userId)
+ .userId(systemUserId)
.deptId(employee.getDeptId())
.date(clockDate)
.clockInTime(clockTime)
.clockInType(clockInType)
- .location(location)
.dataSource(HrmAttendanceDataSourceEnum.CLOCK.getType())
.build();
+ applyLeaveInfo(record, leaves, rule, clockDate);
+ applyClockLocation(record, rule, clockLocation, legacyLocation, true);
attendanceRecordMapper.insert(record);
} else {
record.setClockInTime(clockTime);
record.setClockInType(clockInType);
- if (location != null) {
- record.setLocation(location);
- }
+ applyLeaveInfo(record, leaves, rule, clockDate);
+ applyClockLocation(record, rule, clockLocation, legacyLocation, true);
attendanceRecordMapper.updateById(record);
}
- return record;
+ return new HrmAttendanceClockResult(record, rule,
+ clockLocation != null ? clockLocation.distanceMeters() : null);
}
@Override
@Transactional(rollbackFor = Exception.class)
- public HrmAttendanceRecordDO clockOut(Long userId, LocalDateTime clockTime, String location) {
+ public HrmAttendanceClockResult clockOut(Long userId, LocalDateTime clockTime,
+ HrmAttendanceClockReqVO reqVO, String legacyLocation) {
LocalDate clockDate = clockTime.toLocalDate();
// 1. 鏍¢獙鏄惁宸ヤ綔鏃�
if (!attendanceHolidayService.isWorkDay(clockDate)) {
@@ -132,40 +526,55 @@
if (employee == null) {
throw exception(EMPLOYEE_NOT_EXISTS);
}
+ Long systemUserId = employee.getUserId();
HrmAttendanceRuleDO rule = attendanceRuleService.getAttendanceRuleByDeptId(employee.getDeptId());
if (rule == null) {
throw exception(ATTENDANCE_CLOCK_FAIL_NO_RULE);
}
- // 3. 鏌ヨ褰撴棩鑰冨嫟璁板綍
- HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(userId, clockDate);
+ // 3. 鏌ヨ褰撴棩鑰冨嫟璁板綍锛堢郴缁熺敤鎴� ID锛�
+ HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(systemUserId, clockDate);
+ if (record != null && record.getClockOutTime() != null) {
+ throw exception(ATTENDANCE_CLOCK_ALREADY_CLOCKED);
+ }
+ // 瀹氫綅涓嶅悎娉曟椂蹇呴』鍦ㄥ垱寤衡�滅己涓婄彮鍗♀�濊褰曞墠澶辫触锛岄伩鍏嶇暀涓嬭剰鏁版嵁銆�
+ ClockLocation clockLocation = validateClockLocation(rule, reqVO);
if (record == null) {
- // 濡傛灉娌℃湁涓婄彮鎵撳崱璁板綍锛屽垱寤轰竴鏉$己鍗$殑璁板綍
record = HrmAttendanceRecordDO.builder()
- .userId(userId)
+ .userId(systemUserId)
.deptId(employee.getDeptId())
.date(clockDate)
.clockInType(HrmAttendanceClockTypeEnum.MISSING.getType())
.dataSource(HrmAttendanceDataSourceEnum.CLOCK.getType())
.build();
- attendanceRecordMapper.insert(record);
}
- if (record.getClockOutTime() != null) {
- throw exception(ATTENDANCE_CLOCK_ALREADY_CLOCKED);
+ // 3.1 褰撳ぉ璇峰亣鎯呭喌锛氳鍋囧綋澶╁厑璁告墦鍗★紝涓嶅垽瀹氫负鏃╅��锛屼篃涓嶇畻缂轰笂鐝崱
+ // 鍚屼笂锛氱敤绯荤粺鐢ㄦ埛 ID 鏌ヨ鍋囪〃
+ List<cn.iocoder.yudao.module.hrm.dal.dataobject.approval.HrmLeaveApplicationDO> leaves =
+ leaveQueryService.getApprovedLeaveList(employee.getUserId(), clockDate);
+ boolean onLeave = !leaves.isEmpty();
+ if (onLeave && HrmAttendanceClockTypeEnum.MISSING.getType().equals(record.getClockInType())) {
+ // 璇峰亣褰撳ぉ娌℃墦涓婄彮鍗″睘浜庢甯革紙浜烘病鏉ワ級锛屼笉搴旇缂哄崱
+ record.setClockInType(HrmAttendanceClockTypeEnum.LEAVE.getType());
}
// 4. 鍒ゆ柇鎵撳崱绫诲瀷
LocalTime workEndTime = rule.getWorkEndTime();
int earlyAllowMinutes = rule.getEarlyAllowMinutes() != null ? rule.getEarlyAllowMinutes() : 0;
LocalTime earlyThreshold = workEndTime.minusMinutes(earlyAllowMinutes);
LocalTime clockTimeOnly = clockTime.toLocalTime();
- Integer clockOutType = clockTimeOnly.isAfter(earlyThreshold) || clockTimeOnly.equals(earlyThreshold)
- ? HrmAttendanceClockTypeEnum.NORMAL.getType()
- : HrmAttendanceClockTypeEnum.EARLY.getType();
+ Integer clockOutType;
+ if (onLeave) {
+ // 璇峰亣褰撳ぉ锛氭湁鎵撳崱灏辩畻姝e父
+ clockOutType = HrmAttendanceClockTypeEnum.NORMAL.getType();
+ } else {
+ clockOutType = clockTimeOnly.isAfter(earlyThreshold) || clockTimeOnly.equals(earlyThreshold)
+ ? HrmAttendanceClockTypeEnum.NORMAL.getType()
+ : HrmAttendanceClockTypeEnum.EARLY.getType();
+ }
// 5. 鏇存柊鑰冨嫟璁板綍
record.setClockOutTime(clockTime);
record.setClockOutType(clockOutType);
- if (location != null) {
- record.setLocation(location);
- }
+ applyLeaveInfo(record, leaves, rule, clockDate);
+ applyClockLocation(record, rule, clockLocation, legacyLocation, false);
// 6. 璁$畻宸ヤ綔鏃堕暱
if (record.getClockInTime() != null) {
Duration duration = Duration.between(record.getClockInTime(), clockTime);
@@ -173,8 +582,102 @@
.divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP);
record.setWorkHours(workHours);
}
- attendanceRecordMapper.updateById(record);
- return record;
+ if (record.getId() == null) {
+ attendanceRecordMapper.insert(record);
+ } else {
+ attendanceRecordMapper.updateById(record);
+ }
+ return new HrmAttendanceClockResult(record, rule,
+ clockLocation != null ? clockLocation.distanceMeters() : null);
+ }
+
+ record ClockLocation(BigDecimal longitude, BigDecimal latitude, BigDecimal accuracy,
+ BigDecimal distanceMeters) { }
+
+ ClockLocation validateClockLocation(HrmAttendanceRuleDO rule, HrmAttendanceClockReqVO reqVO) {
+ boolean required = Boolean.TRUE.equals(rule.getLocationRequired());
+ if (required && (StrUtil.isBlank(rule.getLocationName()) || rule.getLongitude() == null
+ || rule.getLatitude() == null || rule.getRadiusMeters() == null
+ || rule.getRadiusMeters() < 10 || rule.getRadiusMeters() > 5000
+ || !"WGS84".equalsIgnoreCase(rule.getCoordinateSystem()))) {
+ throw exception(ATTENDANCE_LOCATION_RULE_INVALID);
+ }
+ if (reqVO == null || reqVO.getLongitude() == null || reqVO.getLatitude() == null
+ || reqVO.getAccuracy() == null || StrUtil.isBlank(reqVO.getCoordinateSystem())) {
+ if (required) {
+ throw exception(ATTENDANCE_LOCATION_REQUIRED);
+ }
+ return null;
+ }
+ boolean coordinatesValid = reqVO.getLongitude().compareTo(BigDecimal.valueOf(-180)) >= 0
+ && reqVO.getLongitude().compareTo(BigDecimal.valueOf(180)) <= 0
+ && reqVO.getLatitude().compareTo(BigDecimal.valueOf(-90)) >= 0
+ && reqVO.getLatitude().compareTo(BigDecimal.valueOf(90)) <= 0;
+ if (!coordinatesValid) {
+ if (required) {
+ throw exception(ATTENDANCE_LOCATION_COORDINATE_INVALID);
+ }
+ return null;
+ }
+ if (reqVO.getAccuracy().signum() <= 0
+ || reqVO.getAccuracy().compareTo(BigDecimal.valueOf(100)) > 0) {
+ if (required) {
+ throw exception(ATTENDANCE_LOCATION_ACCURACY_INVALID);
+ }
+ return null;
+ }
+ if (!"WGS84".equalsIgnoreCase(reqVO.getCoordinateSystem().trim())) {
+ if (required) {
+ throw exception(ATTENDANCE_LOCATION_SYSTEM_INVALID);
+ }
+ return null;
+ }
+ if (!required) {
+ return new ClockLocation(reqVO.getLongitude(), reqVO.getLatitude(), reqVO.getAccuracy(), null);
+ }
+ double rawDistance = haversineMeters(rule.getLongitude().doubleValue(), rule.getLatitude().doubleValue(),
+ reqVO.getLongitude().doubleValue(), reqVO.getLatitude().doubleValue());
+ BigDecimal roundedDistance = BigDecimal.valueOf(rawDistance).setScale(2, RoundingMode.HALF_UP);
+ if (rawDistance > rule.getRadiusMeters()) {
+ throw exception(ATTENDANCE_LOCATION_OUT_OF_RANGE, rule.getLocationName(),
+ roundedDistance.toPlainString(), rule.getRadiusMeters());
+ }
+ return new ClockLocation(reqVO.getLongitude(), reqVO.getLatitude(), reqVO.getAccuracy(), roundedDistance);
+ }
+
+ static double haversineMeters(double lon1, double lat1, double lon2, double lat2) {
+ double latDelta = Math.toRadians(lat2 - lat1);
+ double lonDelta = Math.toRadians(lon2 - lon1);
+ double a = Math.pow(Math.sin(latDelta / 2), 2)
+ + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
+ * Math.pow(Math.sin(lonDelta / 2), 2);
+ double bounded = Math.max(0D, Math.min(1D, a));
+ return 6_371_000D * 2D * Math.atan2(Math.sqrt(bounded), Math.sqrt(1D - bounded));
+ }
+
+ private void applyClockLocation(HrmAttendanceRecordDO record, HrmAttendanceRuleDO rule,
+ ClockLocation location, String legacyLocation, boolean clockIn) {
+ if (StrUtil.isNotBlank(rule.getLocationName())) {
+ record.setLocation(rule.getLocationName());
+ } else if (!Boolean.TRUE.equals(rule.getLocationRequired()) && StrUtil.isNotBlank(legacyLocation)) {
+ record.setLocation(legacyLocation);
+ }
+ if (location == null) {
+ return;
+ }
+ if (clockIn) {
+ record.setClockInLongitude(location.longitude());
+ record.setClockInLatitude(location.latitude());
+ record.setClockInAccuracy(location.accuracy());
+ record.setClockInDistance(location.distanceMeters());
+ record.setClockInAddress(rule.getLocationAddress());
+ } else {
+ record.setClockOutLongitude(location.longitude());
+ record.setClockOutLatitude(location.latitude());
+ record.setClockOutAccuracy(location.accuracy());
+ record.setClockOutDistance(location.distanceMeters());
+ record.setClockOutAddress(rule.getLocationAddress());
+ }
}
@Override
@@ -223,4 +726,4 @@
attendanceRecordMapper.updateById(record);
}
-}
\ No newline at end of file
+}
--
Gitblit v1.9.3