feat(hrm): 支持定位范围打卡
(cherry picked from commit 997d864a76bc718d49aee6cb41d9412ac2a88d73)
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | -- èå¤å®ä½å段è¿ç§»ãæ§è§åä¿æ location_required=0ï¼å
é
ç½®å°ç¹åå¯ç¨èå´éå¶ã |
| | | DELIMITER $$ |
| | | DROP PROCEDURE IF EXISTS xgdl_hrm_attendance_add_column_20261024$$ |
| | | CREATE PROCEDURE xgdl_hrm_attendance_add_column_20261024( |
| | | IN p_table_name VARCHAR(64), IN p_column_name VARCHAR(64), IN p_ddl_sql VARCHAR(1000)) |
| | | BEGIN |
| | | IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS |
| | | WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = p_table_name AND COLUMN_NAME = p_column_name) THEN |
| | | SET @hrm_location_ddl = p_ddl_sql; |
| | | PREPARE hrm_location_stmt FROM @hrm_location_ddl; |
| | | EXECUTE hrm_location_stmt; |
| | | DEALLOCATE PREPARE hrm_location_stmt; |
| | | END IF; |
| | | END$$ |
| | | DELIMITER ; |
| | | |
| | | CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_rule','location_name', |
| | | 'ALTER TABLE hrm_attendance_rule ADD COLUMN location_name varchar(100) NULL'); |
| | | CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_rule','location_address', |
| | | 'ALTER TABLE hrm_attendance_rule ADD COLUMN location_address varchar(255) NULL'); |
| | | CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_rule','longitude', |
| | | 'ALTER TABLE hrm_attendance_rule ADD COLUMN longitude decimal(10,7) NULL'); |
| | | CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_rule','latitude', |
| | | 'ALTER TABLE hrm_attendance_rule ADD COLUMN latitude decimal(10,7) NULL'); |
| | | CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_rule','radius_meters', |
| | | 'ALTER TABLE hrm_attendance_rule ADD COLUMN radius_meters int DEFAULT 10'); |
| | | CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_rule','location_required', |
| | | 'ALTER TABLE hrm_attendance_rule ADD COLUMN location_required bit(1) NOT NULL DEFAULT b''0'''); |
| | | CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_rule','coordinate_system', |
| | | 'ALTER TABLE hrm_attendance_rule ADD COLUMN coordinate_system varchar(20) DEFAULT ''WGS84'''); |
| | | |
| | | CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_record','clock_in_longitude', |
| | | 'ALTER TABLE hrm_attendance_record ADD COLUMN clock_in_longitude decimal(10,7) NULL'); |
| | | CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_record','clock_in_latitude', |
| | | 'ALTER TABLE hrm_attendance_record ADD COLUMN clock_in_latitude decimal(10,7) NULL'); |
| | | CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_record','clock_in_accuracy', |
| | | 'ALTER TABLE hrm_attendance_record ADD COLUMN clock_in_accuracy decimal(8,2) NULL'); |
| | | CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_record','clock_in_distance', |
| | | 'ALTER TABLE hrm_attendance_record ADD COLUMN clock_in_distance decimal(10,2) NULL'); |
| | | CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_record','clock_in_address', |
| | | 'ALTER TABLE hrm_attendance_record ADD COLUMN clock_in_address varchar(255) NULL'); |
| | | CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_record','clock_out_longitude', |
| | | 'ALTER TABLE hrm_attendance_record ADD COLUMN clock_out_longitude decimal(10,7) NULL'); |
| | | CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_record','clock_out_latitude', |
| | | 'ALTER TABLE hrm_attendance_record ADD COLUMN clock_out_latitude decimal(10,7) NULL'); |
| | | CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_record','clock_out_accuracy', |
| | | 'ALTER TABLE hrm_attendance_record ADD COLUMN clock_out_accuracy decimal(8,2) NULL'); |
| | | CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_record','clock_out_distance', |
| | | 'ALTER TABLE hrm_attendance_record ADD COLUMN clock_out_distance decimal(10,2) NULL'); |
| | | CALL xgdl_hrm_attendance_add_column_20261024('hrm_attendance_record','clock_out_address', |
| | | 'ALTER TABLE hrm_attendance_record ADD COLUMN clock_out_address varchar(255) NULL'); |
| | | |
| | | DROP PROCEDURE xgdl_hrm_attendance_add_column_20261024; |
| | | |
| | | -- 对é½èå¤è§å䏿塿鮿éç ï¼ç¡®ä¿ Web å APP æ®éè§è²å¯ä»¥ææä½¿ç¨ã |
| | | UPDATE `system_menu` SET `permission` = 'hrm:attendance-rule:query' WHERE `id` = 60131; |
| | | UPDATE `system_menu` SET `permission` = 'hrm:attendance-rule:create' WHERE `id` = 60132; |
| | | UPDATE `system_menu` SET `permission` = 'hrm:attendance-rule:update' WHERE `id` = 60133; |
| | | UPDATE `system_menu` SET `permission` = 'hrm:attendance-rule:delete' WHERE `id` = 60134; |
| | | UPDATE `system_menu` SET `permission` = 'hrm:attendance-rule:export' WHERE `id` = 60135; |
| | | |
| | | INSERT INTO `system_menu` |
| | | (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, |
| | | `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, |
| | | `updater`, `update_time`, `deleted`) |
| | | VALUES |
| | | (60114, 'å®ä½æå¡', 'hrm:attendance-record:clock', 3, 4, 6011, '', '', '', '', |
| | | 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0') |
| | | ON DUPLICATE KEY UPDATE |
| | | `name` = VALUES(`name`), `permission` = VALUES(`permission`), `status` = 0, `deleted` = b'0'; |
| | | |
| | | -- å·²æ¥æâèå¤è®°å½âèåçè§è²èªå¨è·å¾å®ä½æå¡æé®æéã |
| | | INSERT INTO `system_role_menu` |
| | | (`role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) |
| | | SELECT DISTINCT parent_role.`role_id`, 60114, '1', NOW(), '1', NOW(), b'0', parent_role.`tenant_id` |
| | | FROM `system_role_menu` parent_role |
| | | WHERE parent_role.`menu_id` = 6011 AND parent_role.`deleted` = b'0' |
| | | AND NOT EXISTS ( |
| | | SELECT 1 FROM `system_role_menu` current_grant |
| | | WHERE current_grant.`role_id` = parent_role.`role_id` |
| | | AND current_grant.`menu_id` = 60114 |
| | | AND current_grant.`tenant_id` = parent_role.`tenant_id` |
| | | AND current_grant.`deleted` = b'0' |
| | | ); |
| | |
| | | ErrorCode ATTENDANCE_EXCEPTION_BPM_PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_030_004_015, "èå¤å¼å¸¸å®¡æ¹æµç¨ä¸åå¨"); |
| | | ErrorCode ATTENDANCE_CLOCK_ALREADY_CLOCKED = new ErrorCode(1_030_004_016, "å·²æè¿å¡ï¼è¯·å¿é夿å¡"); |
| | | ErrorCode ATTENDANCE_RULE_EFFECTIVE_DATE_INVALID = new ErrorCode(1_030_004_017, "çæå¼å§æ¥æä¸è½æäºçæç»ææ¥æ"); |
| | | ErrorCode ATTENDANCE_HOLIDAY_DATE_RANGE_INVALID = new ErrorCode(1_030_004_017, "ç»ææ¥æã{}ãä¸è½æ©äºå¼å§æ¥æã{}ãï¼æ æ³ä¿åãè¯·éæ°éæ©æ£ç¡®çæ¥æåºé´"); |
| | | ErrorCode ATTENDANCE_LOCATION_RULE_INVALID = new ErrorCode(1_030_004_050, "èå¤è§åå°ç¹é
ç½®ä¸å®æ´ï¼è¯·è系管çå"); |
| | | ErrorCode ATTENDANCE_LOCATION_REQUIRED = new ErrorCode(1_030_004_051, "请å¼å¯å®ä½å¹¶æä¾ç»çº¬åº¦ã精度ååæ ç³»åæå¡"); |
| | | ErrorCode ATTENDANCE_LOCATION_COORDINATE_INVALID = new ErrorCode(1_030_004_052, "æå¡ç»çº¬åº¦è¶
åºææèå´"); |
| | | ErrorCode ATTENDANCE_LOCATION_ACCURACY_INVALID = new ErrorCode(1_030_004_053, "å®ä½ç²¾åº¦é¡»å¤§äº 0 ä¸ä¸è¶
è¿ 100 ç±³ï¼è¯·å°å¼éä½ç½®éè¯"); |
| | | ErrorCode ATTENDANCE_LOCATION_SYSTEM_INVALID = new ErrorCode(1_030_004_054, "æå¡åæ ç³»å¿
须为 WGS84"); |
| | | ErrorCode ATTENDANCE_LOCATION_OUT_OF_RANGE = new ErrorCode(1_030_004_055, "å½åä½ç½®è·â{}â{}ç±³ï¼è¶
è¿å
许èå´{}ç±³"); |
| | | |
| | | // ========== èªé
¬ç®¡ç 1-030-005-000 ========== |
| | | ErrorCode SALARY_STRUCTURE_NOT_EXISTS = new ErrorCode(1_030_005_000, "èªé
¬ç»æä¸åå¨"); |
| | |
| | | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; |
| | | import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceClockReqVO; |
| | | import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceClockRespVO; |
| | | 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.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.HrmAttendanceClockResult; |
| | | 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; |
| | |
| | | @PostMapping("/clock-in") |
| | | @Operation(summary = "ä¸çæå¡") |
| | | @PreAuthorize("@ss.hasPermission('hrm:attendance-record:clock')") |
| | | public CommonResult<HrmAttendanceRecordRespVO> clockIn(@RequestParam("location") String location) { |
| | | @ApiAccessLog(requestEnable = false) |
| | | public CommonResult<HrmAttendanceClockRespVO> clockIn( |
| | | @Valid @RequestBody(required = false) HrmAttendanceClockReqVO reqVO, |
| | | @RequestParam(value = "location", required = false) String legacyLocation) { |
| | | Long loginUserId = getLoginUserId(); |
| | | HrmEmployeeDO employee = employeeService.getEmployeeByUserId(loginUserId); |
| | | if (employee == null) { |
| | | throw new ServiceException(EMPLOYEE_NOT_EXISTS.getCode(), |
| | | "å½åç»å½è´¦å·æªå
³èå工信æ¯ï¼æ æ³æå¡ã请è系管çåå¨ãå工管çãä¸å°è¯¥ç»å½è´¦å·ç»å®å°åå·¥ã"); |
| | | } |
| | | 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); |
| | | HrmAttendanceClockResult result = attendanceRecordService.clockIn( |
| | | employee.getId(), LocalDateTime.now(), reqVO, legacyLocation); |
| | | return success(buildClockResultVO(result, employee)); |
| | | } |
| | | |
| | | @PostMapping("/clock-out") |
| | | @Operation(summary = "ä¸çæå¡") |
| | | @PreAuthorize("@ss.hasPermission('hrm:attendance-record:clock')") |
| | | public CommonResult<HrmAttendanceRecordRespVO> clockOut(@RequestParam("location") String location) { |
| | | @ApiAccessLog(requestEnable = false) |
| | | public CommonResult<HrmAttendanceClockRespVO> clockOut( |
| | | @Valid @RequestBody(required = false) HrmAttendanceClockReqVO reqVO, |
| | | @RequestParam(value = "location", required = false) String legacyLocation) { |
| | | Long loginUserId = getLoginUserId(); |
| | | HrmEmployeeDO employee = employeeService.getEmployeeByUserId(loginUserId); |
| | | if (employee == null) { |
| | | throw new ServiceException(EMPLOYEE_NOT_EXISTS.getCode(), |
| | | "å½åç»å½è´¦å·æªå
³èå工信æ¯ï¼æ æ³æå¡ã请è系管çåå¨ãå工管çãä¸å°è¯¥ç»å½è´¦å·ç»å®å°åå·¥ã"); |
| | | } |
| | | HrmAttendanceRecordDO record = attendanceRecordService.clockOut(employee.getId(), LocalDateTime.now(), location); |
| | | HrmAttendanceClockResult result = attendanceRecordService.clockOut( |
| | | employee.getId(), LocalDateTime.now(), reqVO, legacyLocation); |
| | | return success(buildClockResultVO(result, employee)); |
| | | } |
| | | |
| | | private HrmAttendanceRecordRespVO buildClockRespVO(HrmAttendanceRecordDO record, HrmEmployeeDO employee) { |
| | | HrmAttendanceRecordRespVO respVO = BeanUtils.toBean(record, HrmAttendanceRecordRespVO.class); |
| | | respVO.setUserName(employee.getName()); |
| | | if (record.getDeptId() != null) { |
| | |
| | | respVO.setDeptName(dept.getName()); |
| | | } |
| | | } |
| | | return success(respVO); |
| | | return respVO; |
| | | } |
| | | |
| | | private HrmAttendanceClockRespVO buildClockResultVO(HrmAttendanceClockResult result, |
| | | HrmEmployeeDO employee) { |
| | | HrmAttendanceClockRespVO respVO = new HrmAttendanceClockRespVO(); |
| | | respVO.setRecord(buildClockRespVO(result.record(), employee)); |
| | | respVO.setLocationName(result.rule().getLocationName()); |
| | | if (Boolean.TRUE.equals(result.rule().getLocationRequired())) { |
| | | respVO.setDistanceMeters(result.distanceMeters()); |
| | | respVO.setRadiusMeters(result.rule().getRadiusMeters()); |
| | | respVO.setWithinRange(true); |
| | | } |
| | | return respVO; |
| | | } |
| | | |
| | | @GetMapping("/my-status") |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo; |
| | | |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | import lombok.Data; |
| | | |
| | | import java.math.BigDecimal; |
| | | |
| | | /** App/Web æå¡å®ä½åå§æ°æ®ï¼æ¯å¦å¿
å¡«ç±å½åçæè§åå³å®ã */ |
| | | @Data |
| | | public class HrmAttendanceClockReqVO { |
| | | @Schema(description = "设å¤ç»åº¦ï¼WGS84ï¼") |
| | | private BigDecimal longitude; |
| | | @Schema(description = "设å¤çº¬åº¦ï¼WGS84ï¼") |
| | | private BigDecimal latitude; |
| | | @Schema(description = "å®ä½ç²¾åº¦ï¼ç±³") |
| | | private BigDecimal accuracy; |
| | | @Schema(description = "åæ ç³»ï¼ä»
æ¯æ WGS84") |
| | | private String coordinateSystem; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo; |
| | | |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | import lombok.Data; |
| | | |
| | | import java.math.BigDecimal; |
| | | |
| | | @Data |
| | | public class HrmAttendanceClockRespVO { |
| | | @Schema(description = "èå¤è®°å½") |
| | | private HrmAttendanceRecordRespVO record; |
| | | @Schema(description = "è§åå°ç¹åç§°") |
| | | private String locationName; |
| | | @Schema(description = "è·è§åå°ç¹è·ç¦»ï¼ç±³ï¼æªå¯ç¨ä½ç½®éå¶æ¶ä¸ºç©º") |
| | | private BigDecimal distanceMeters; |
| | | @Schema(description = "å
许åå¾ï¼ç±³ï¼æªå¯ç¨ä½ç½®éå¶æ¶ä¸ºç©º") |
| | | private Integer radiusMeters; |
| | | @Schema(description = "æ¯å¦å¨å
许èå´ï¼æªå¯ç¨ä½ç½®éå¶æ¶ä¸ºç©º") |
| | | private Boolean withinRange; |
| | | } |
| | |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | import lombok.Data; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | |
| | |
| | | @Schema(description = "彿¥èå¤è®°å½ï¼æªæå¡æ¶ä¸º null") |
| | | private HrmAttendanceRecordRespVO record; |
| | | |
| | | @Schema(description = "æ¯å¦å¯ç¨å®ä½èå´æ ¡éª") |
| | | private Boolean locationRequired; |
| | | |
| | | @Schema(description = "æå¡å°ç¹åç§°") |
| | | private String locationName; |
| | | |
| | | @Schema(description = "æå¡å°ç¹å°å") |
| | | private String locationAddress; |
| | | |
| | | @Schema(description = "æå¡ä¸å¿ç»åº¦ï¼WGS84ï¼") |
| | | private BigDecimal longitude; |
| | | |
| | | @Schema(description = "æå¡ä¸å¿çº¬åº¦ï¼WGS84ï¼") |
| | | private BigDecimal latitude; |
| | | |
| | | @Schema(description = "å
许æå¡åå¾ï¼åä½ç±³") |
| | | private Integer radiusMeters; |
| | | |
| | | @Schema(description = "åæ ç³»") |
| | | private String coordinateSystem; |
| | | |
| | | @Schema(description = "仿¥è®°å½IDï¼æªæå¡æ¶ä¸ºç©º") |
| | | private Long recordId; |
| | | |
| | | @Schema(description = "å½å坿å¡ç¶æ") |
| | | private Clockable clockable; |
| | | |
| | |
| | | @ExcelProperty("æå¡å°ç¹") |
| | | private String location; |
| | | |
| | | @Schema(description = "ä¸çæå¡ç»åº¦ï¼WGS84ï¼") |
| | | private BigDecimal clockInLongitude; |
| | | @Schema(description = "ä¸çæå¡çº¬åº¦ï¼WGS84ï¼") |
| | | private BigDecimal clockInLatitude; |
| | | @Schema(description = "ä¸çå®ä½ç²¾åº¦ï¼ç±³") |
| | | private BigDecimal clockInAccuracy; |
| | | @Schema(description = "ä¸çè·è§åå°ç¹è·ç¦»ï¼ç±³") |
| | | private BigDecimal clockInDistance; |
| | | @Schema(description = "ä¸çè§åå°ç¹å°åå¿«ç
§") |
| | | private String clockInAddress; |
| | | @Schema(description = "ä¸çæå¡ç»åº¦ï¼WGS84ï¼") |
| | | private BigDecimal clockOutLongitude; |
| | | @Schema(description = "ä¸çæå¡çº¬åº¦ï¼WGS84ï¼") |
| | | private BigDecimal clockOutLatitude; |
| | | @Schema(description = "ä¸çå®ä½ç²¾åº¦ï¼ç±³") |
| | | private BigDecimal clockOutAccuracy; |
| | | @Schema(description = "ä¸çè·è§åå°ç¹è·ç¦»ï¼ç±³") |
| | | private BigDecimal clockOutDistance; |
| | | @Schema(description = "ä¸çè§åå°ç¹å°åå¿«ç
§") |
| | | private String clockOutAddress; |
| | | |
| | | @Schema(description = "夿³¨") |
| | | @ExcelProperty("夿³¨") |
| | | private String remark; |
| | |
| | | @Schema(description = "çæç»ææ¥æï¼ç©ºè¡¨ç¤ºä¸éï¼") |
| | | @ExcelProperty("çæç»ææ¥æ") |
| | | private LocalDate effectiveEndDate; |
| | | @Schema(description = "æå¡å°ç¹åç§°") |
| | | private String locationName; |
| | | @Schema(description = "æå¡å°ç¹å°å") |
| | | private String locationAddress; |
| | | @Schema(description = "å°ç¹ç»åº¦ï¼WGS84ï¼") |
| | | private BigDecimal longitude; |
| | | @Schema(description = "å°ç¹çº¬åº¦ï¼WGS84ï¼") |
| | | private BigDecimal latitude; |
| | | @Schema(description = "å
许æå¡åå¾ï¼åä½ç±³") |
| | | private Integer radiusMeters; |
| | | @Schema(description = "æ¯å¦å¯ç¨ä½ç½®éå¶") |
| | | private Boolean locationRequired; |
| | | @Schema(description = "åæ ç³»ï¼ä»
æ¯æ WGS84") |
| | | private String coordinateSystem; |
| | | |
| | | @Schema(description = "ç¶æï¼0-å¯ç¨ï¼1-ç¦ç¨") |
| | | @ExcelProperty(value = "ç¶æ", converter = DictConvert.class) |
| | |
| | | |
| | | import jakarta.validation.constraints.NotBlank; |
| | | import jakarta.validation.constraints.NotNull; |
| | | import jakarta.validation.constraints.AssertTrue; |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalTime; |
| | |
| | | |
| | | @Schema(description = "çæç»ææ¥æï¼ç©ºè¡¨ç¤ºä¸éï¼", example = "2026-12-31") |
| | | private LocalDate effectiveEndDate; |
| | | @Schema(description = "æå¡å°ç¹åç§°") |
| | | private String locationName; |
| | | @Schema(description = "æå¡å°ç¹å°å") |
| | | private String locationAddress; |
| | | @Schema(description = "å°ç¹ç»åº¦ï¼WGS84ï¼") |
| | | private BigDecimal longitude; |
| | | @Schema(description = "å°ç¹çº¬åº¦ï¼WGS84ï¼") |
| | | private BigDecimal latitude; |
| | | @Schema(description = "å
许æå¡åå¾ï¼åä½ç±³") |
| | | private Integer radiusMeters; |
| | | @Schema(description = "æ¯å¦å¯ç¨ä½ç½®éå¶") |
| | | private Boolean locationRequired; |
| | | @Schema(description = "åæ ç³»ï¼ä»
æ¯æ WGS84") |
| | | private String coordinateSystem; |
| | | |
| | | @AssertTrue(message = "å¯ç¨å®ä½æå¡æ¶ï¼å°ç¹åç§°ãç»çº¬åº¦å10~5000ç±³æå¡åå¾å¿
须宿´é
ç½®") |
| | | public boolean isLocationConfigValid() { |
| | | if (!Boolean.TRUE.equals(locationRequired)) { |
| | | return true; |
| | | } |
| | | return locationName != null && !locationName.isBlank() |
| | | && longitude != null && latitude != null && radiusMeters != null |
| | | && radiusMeters >= 10 && radiusMeters <= 5000 |
| | | && "WGS84".equalsIgnoreCase(coordinateSystem); |
| | | } |
| | | |
| | | @Schema(description = "ç¶æï¼0-å¯ç¨ï¼1-ç¦ç¨", example = "0") |
| | | private Integer status; |
| | |
| | | */ |
| | | private String location; |
| | | |
| | | private BigDecimal clockInLongitude; |
| | | private BigDecimal clockInLatitude; |
| | | private BigDecimal clockInAccuracy; |
| | | private BigDecimal clockInDistance; |
| | | private String clockInAddress; |
| | | private BigDecimal clockOutLongitude; |
| | | private BigDecimal clockOutLatitude; |
| | | private BigDecimal clockOutAccuracy; |
| | | private BigDecimal clockOutDistance; |
| | | private String clockOutAddress; |
| | | |
| | | /** |
| | | * 夿³¨ |
| | | */ |
| | |
| | | */ |
| | | private BigDecimal overtimeThreshold; |
| | | |
| | | /** èå¤å°ç¹ä¸å¯æå¡èå´ï¼æ§è§åé»è®¤ä¸å¯ç¨ä½ç½®éå¶ã */ |
| | | private String locationName; |
| | | private String locationAddress; |
| | | private BigDecimal longitude; |
| | | private BigDecimal latitude; |
| | | private Integer radiusMeters; |
| | | private Boolean locationRequired; |
| | | private String coordinateSystem; |
| | | |
| | | /** |
| | | * çæå¼å§æ¥æï¼ç©ºè¡¨ç¤ºä¸éï¼ |
| | | */ |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.hrm.service.attendance; |
| | | |
| | | import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRecordDO; |
| | | import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRuleDO; |
| | | |
| | | import java.math.BigDecimal; |
| | | |
| | | /** æå¡ä¿åç»æåæ¬æ¬¡æ ¡éªä½¿ç¨çè§åå¿«ç
§ã */ |
| | | public record HrmAttendanceClockResult(HrmAttendanceRecordDO record, HrmAttendanceRuleDO rule, |
| | | BigDecimal distanceMeters) { |
| | | } |
| | |
| | | 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.HrmAttendanceClockReqVO; |
| | | 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; |
| | |
| | | * |
| | | * @param userId åå·¥ID |
| | | * @param clockTime æå¡æ¶é´ |
| | | * @param location æå¡å°ç¹ |
| | | * @return èå¤è®°å½ |
| | | * @param reqVO åå§å®ä½ä¿¡æ¯ï¼WGS84ï¼ |
| | | * @param legacyLocation æ§çå°ç¹ææ¬ï¼ä»
卿ªå¯ç¨ä½ç½®éå¶æ¶å
¼å®¹ä½¿ç¨ |
| | | * @return æå¡è®°å½ãè§ååè·ç¦» |
| | | */ |
| | | HrmAttendanceRecordDO clockIn(Long userId, LocalDateTime clockTime, String location); |
| | | HrmAttendanceClockResult clockIn(Long userId, LocalDateTime clockTime, |
| | | HrmAttendanceClockReqVO reqVO, String legacyLocation); |
| | | |
| | | /** |
| | | * æå¡ï¼ä¸çæå¡ï¼ |
| | | * |
| | | * @param userId åå·¥ID |
| | | * @param clockTime æå¡æ¶é´ |
| | | * @param location æå¡å°ç¹ |
| | | * @return èå¤è®°å½ |
| | | * @param reqVO åå§å®ä½ä¿¡æ¯ï¼WGS84ï¼ |
| | | * @param legacyLocation æ§çå°ç¹ææ¬ï¼ä»
卿ªå¯ç¨ä½ç½®éå¶æ¶å
¼å®¹ä½¿ç¨ |
| | | * @return æå¡è®°å½ãè§ååè·ç¦» |
| | | */ |
| | | HrmAttendanceRecordDO clockOut(Long userId, LocalDateTime clockTime, String location); |
| | | HrmAttendanceClockResult clockOut(Long userId, LocalDateTime clockTime, |
| | | HrmAttendanceClockReqVO reqVO, String legacyLocation); |
| | | |
| | | /** |
| | | * è¡¥å¡å®¡æ¹éè¿åæ´æ°èå¤è®°å½ |
| | |
| | | package cn.iocoder.yudao.module.hrm.service.attendance; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| | | 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.HrmAttendanceRecordPageReqVO; |
| | | import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceMyStatusRespVO; |
| | | import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordRespVO; |
| | |
| | | |
| | | @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)) { |
| | |
| | | if (record != null && record.getClockInTime() != null) { |
| | | throw exception(ATTENDANCE_CLOCK_ALREADY_CLOCKED); |
| | | } |
| | | // å¨ä»»ä½ååºæä½å宿å®ä½æ ¡éªï¼æ§çå°ç¹å符串ä¸è½ç»è¿èå´éå¶ã |
| | | ClockLocation clockLocation = validateClockLocation(rule, reqVO); |
| | | // 4. 夿æå¡ç±»å |
| | | LocalTime workStartTime = rule.getWorkStartTime(); |
| | | int lateAllowMinutes = rule.getLateAllowMinutes() != null ? rule.getLateAllowMinutes() : 0; |
| | |
| | | .date(clockDate) |
| | | .clockInTime(clockTime) |
| | | .clockInType(clockInType) |
| | | .location(location) |
| | | .dataSource(HrmAttendanceDataSourceEnum.CLOCK.getType()) |
| | | .build(); |
| | | applyClockLocation(record, rule, clockLocation, legacyLocation, true); |
| | | attendanceRecordMapper.insert(record); |
| | | } else { |
| | | record.setClockInTime(clockTime); |
| | | record.setClockInType(clockInType); |
| | | if (location != null) { |
| | | record.setLocation(location); |
| | | } |
| | | 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)) { |
| | |
| | | } |
| | | // 3. æ¥è¯¢å½æ¥èå¤è®°å½ |
| | | HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(userId, 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) |
| | | .deptId(employee.getDeptId()) |
| | |
| | | .clockInType(HrmAttendanceClockTypeEnum.MISSING.getType()) |
| | | .dataSource(HrmAttendanceDataSourceEnum.CLOCK.getType()) |
| | | .build(); |
| | | attendanceRecordMapper.insert(record); |
| | | } |
| | | if (record.getClockOutTime() != null) { |
| | | throw exception(ATTENDANCE_CLOCK_ALREADY_CLOCKED); |
| | | } |
| | | // 4. 夿æå¡ç±»å |
| | | LocalTime workEndTime = rule.getWorkEndTime(); |
| | |
| | | // 5. æ´æ°èå¤è®°å½ |
| | | record.setClockOutTime(clockTime); |
| | | record.setClockOutType(clockOutType); |
| | | if (location != null) { |
| | | record.setLocation(location); |
| | | } |
| | | applyClockLocation(record, rule, clockLocation, legacyLocation, false); |
| | | // 6. 计ç®å·¥ä½æ¶é¿ |
| | | if (record.getClockInTime() != null) { |
| | | Duration duration = Duration.between(record.getClockInTime(), clockTime); |
| | |
| | | .divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP); |
| | | record.setWorkHours(workHours); |
| | | } |
| | | if (record.getId() == null) { |
| | | attendanceRecordMapper.insert(record); |
| | | } else { |
| | | attendanceRecordMapper.updateById(record); |
| | | return 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 |
| | |
| | | } |
| | | } |
| | | respVO.setRule(ruleRespVO); |
| | | // å
¼å®¹æ§ç客æ·ç«¯ï¼ä¿çå¹³éºçå®ä½å段 |
| | | 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()); |
| | | } |
| | | |
| | | // 4. 彿¥èå¤è®°å½ |
| | | HrmAttendanceRecordDO record = attendanceRecordMapper.selectByUserIdAndDate(employee.getId(), queryDate); |
| | | if (record != null) { |
| | | respVO.setRecord(buildRecordRespVO(record, employee)); |
| | | respVO.setRecordId(record.getId()); |
| | | } |
| | | |
| | | // 5. å½å坿å¡ç¶æ |
| | |
| | | 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.framework.mybatis.core.query.LambdaQueryWrapperX; |
| | |
| | | |
| | | @Override |
| | | public Long createAttendanceRule(HrmAttendanceRuleSaveReqVO createReqVO) { |
| | | validateLocation(createReqVO); |
| | | // æ ¡éªè§ååç§°æ¯å¦éå¤ï¼åä¸é¨é¨å
è®¸å¤æ¡è§åï¼ä¸åéå¶é¨é¨å¯ä¸ï¼ |
| | | validateAttendanceRuleNameUnique(null, createReqVO.getName()); |
| | | validateEffectiveDateRange(createReqVO.getEffectiveStartDate(), createReqVO.getEffectiveEndDate()); |
| | |
| | | |
| | | @Override |
| | | public void updateAttendanceRule(HrmAttendanceRuleSaveReqVO updateReqVO) { |
| | | validateAttendanceRuleExists(updateReqVO.getId()); |
| | | HrmAttendanceRuleDO currentRule = attendanceRuleMapper.selectById(updateReqVO.getId()); |
| | | if (currentRule == null) { |
| | | throw exception(ATTENDANCE_RULE_NOT_EXISTS); |
| | | } |
| | | // å
¼å®¹æ§ç®¡çç«¯ï¼æ²¡ææäº¤å°ç¹å段æ¶ï¼ä¸æ¸
空已é
ç½®çä½ç½®éå¶ã |
| | | if (updateReqVO.getLocationRequired() == null) { |
| | | updateReqVO.setLocationRequired(currentRule.getLocationRequired()); |
| | | if (updateReqVO.getLocationName() == null) updateReqVO.setLocationName(currentRule.getLocationName()); |
| | | if (updateReqVO.getLocationAddress() == null) updateReqVO.setLocationAddress(currentRule.getLocationAddress()); |
| | | if (updateReqVO.getLongitude() == null) updateReqVO.setLongitude(currentRule.getLongitude()); |
| | | if (updateReqVO.getLatitude() == null) updateReqVO.setLatitude(currentRule.getLatitude()); |
| | | if (updateReqVO.getRadiusMeters() == null) updateReqVO.setRadiusMeters(currentRule.getRadiusMeters()); |
| | | if (updateReqVO.getCoordinateSystem() == null) updateReqVO.setCoordinateSystem(currentRule.getCoordinateSystem()); |
| | | } |
| | | validateLocation(updateReqVO); |
| | | // æ ¡éªè§ååç§°æ¯å¦éå¤ |
| | | validateAttendanceRuleNameUnique(updateReqVO.getId(), updateReqVO.getName()); |
| | | validateEffectiveDateRange(updateReqVO.getEffectiveStartDate(), updateReqVO.getEffectiveEndDate()); |
| | |
| | | } |
| | | } |
| | | |
| | | private void validateLocation(HrmAttendanceRuleSaveReqVO reqVO) { |
| | | if (reqVO.getLocationRequired() == null) { |
| | | reqVO.setLocationRequired(false); |
| | | } |
| | | if (reqVO.getRadiusMeters() == null) { |
| | | reqVO.setRadiusMeters(10); |
| | | } |
| | | if (StrUtil.isBlank(reqVO.getCoordinateSystem())) { |
| | | reqVO.setCoordinateSystem("WGS84"); |
| | | } else { |
| | | reqVO.setCoordinateSystem(reqVO.getCoordinateSystem().trim().toUpperCase(java.util.Locale.ROOT)); |
| | | } |
| | | if (!"WGS84".equals(reqVO.getCoordinateSystem())) { |
| | | throw exception(ATTENDANCE_LOCATION_SYSTEM_INVALID); |
| | | } |
| | | if (reqVO.getLongitude() != null && (reqVO.getLongitude().compareTo(BigDecimal.valueOf(-180)) < 0 |
| | | || reqVO.getLongitude().compareTo(BigDecimal.valueOf(180)) > 0)) { |
| | | throw exception(ATTENDANCE_LOCATION_COORDINATE_INVALID); |
| | | } |
| | | if (reqVO.getLatitude() != null && (reqVO.getLatitude().compareTo(BigDecimal.valueOf(-90)) < 0 |
| | | || reqVO.getLatitude().compareTo(BigDecimal.valueOf(90)) > 0)) { |
| | | throw exception(ATTENDANCE_LOCATION_COORDINATE_INVALID); |
| | | } |
| | | if (reqVO.getRadiusMeters() < 10 || reqVO.getRadiusMeters() > 5000) { |
| | | throw exception(ATTENDANCE_LOCATION_RULE_INVALID); |
| | | } |
| | | if (Boolean.TRUE.equals(reqVO.getLocationRequired()) |
| | | && (StrUtil.isBlank(reqVO.getLocationName()) || reqVO.getLongitude() == null |
| | | || reqVO.getLatitude() == null)) { |
| | | throw exception(ATTENDANCE_LOCATION_RULE_INVALID); |
| | | } |
| | | } |
| | | |
| | | private void validateAttendanceRuleNameUnique(Long id, String name) { |
| | | HrmAttendanceRuleDO rule = attendanceRuleMapper.selectOne(HrmAttendanceRuleDO::getName, name); |
| | | if (rule == null) { |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package cn.iocoder.yudao.module.hrm.service.attendance; |
| | | |
| | | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| | | import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceClockReqVO; |
| | | import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRuleDO; |
| | | 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.dal.mysql.attendance.HrmAttendanceRecordMapper; |
| | | import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.test.util.ReflectionTestUtils; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | import static org.mockito.ArgumentMatchers.any; |
| | | import static org.mockito.Mockito.*; |
| | | |
| | | class HrmAttendanceClockLocationTest { |
| | | |
| | | private final HrmAttendanceRecordServiceImpl service = new HrmAttendanceRecordServiceImpl(); |
| | | |
| | | private HrmAttendanceRuleDO requiredRule() { |
| | | return HrmAttendanceRuleDO.builder().locationRequired(true).locationName("å
¬å¸åå°") |
| | | .longitude(BigDecimal.ZERO).latitude(BigDecimal.ZERO) |
| | | .radiusMeters(100).coordinateSystem("WGS84").build(); |
| | | } |
| | | |
| | | private HrmAttendanceClockReqVO position(String latitude, String accuracy) { |
| | | HrmAttendanceClockReqVO reqVO = new HrmAttendanceClockReqVO(); |
| | | reqVO.setLongitude(BigDecimal.ZERO); |
| | | reqVO.setLatitude(new BigDecimal(latitude)); |
| | | reqVO.setAccuracy(new BigDecimal(accuracy)); |
| | | reqVO.setCoordinateSystem("WGS84"); |
| | | return reqVO; |
| | | } |
| | | |
| | | @Test |
| | | void oldRuleWithoutLocationStillAcceptsClock() { |
| | | HrmAttendanceRuleDO rule = HrmAttendanceRuleDO.builder().locationRequired(false).build(); |
| | | assertNull(service.validateClockLocation(rule, null)); |
| | | assertNull(service.validateClockLocation(rule, position("0.0001", "20")).distanceMeters()); |
| | | } |
| | | |
| | | @Test |
| | | void requiredLocationValidatesCompletenessCoordinatesAndAccuracy() { |
| | | HrmAttendanceRuleDO rule = requiredRule(); |
| | | assertThrows(ServiceException.class, () -> service.validateClockLocation(rule, null)); |
| | | assertThrows(ServiceException.class, () -> service.validateClockLocation(rule, position("91", "20"))); |
| | | assertThrows(ServiceException.class, () -> service.validateClockLocation(rule, position("0", "100.01"))); |
| | | assertNotNull(service.validateClockLocation(rule, position("0", "100"))); |
| | | } |
| | | |
| | | @Test |
| | | void comparesUnroundedDistanceWithRadius() { |
| | | HrmAttendanceRuleDO rule = requiredRule(); |
| | | assertTrue(HrmAttendanceRecordServiceImpl.haversineMeters(0, 0, 0, 0.00089) < 100); |
| | | assertNotNull(service.validateClockLocation(rule, position("0.00089", "20"))); |
| | | assertThrows(ServiceException.class, () -> service.validateClockLocation(rule, position("0.00091", "20"))); |
| | | } |
| | | |
| | | @Test |
| | | void invalidClockOutLocationDoesNotCreateMissingClockInRecord() { |
| | | HrmAttendanceHolidayService holidayService = mock(HrmAttendanceHolidayService.class); |
| | | HrmEmployeeService employeeService = mock(HrmEmployeeService.class); |
| | | HrmAttendanceRuleService ruleService = mock(HrmAttendanceRuleService.class); |
| | | HrmAttendanceRecordMapper mapper = mock(HrmAttendanceRecordMapper.class); |
| | | ReflectionTestUtils.setField(service, "attendanceHolidayService", holidayService); |
| | | ReflectionTestUtils.setField(service, "employeeService", employeeService); |
| | | ReflectionTestUtils.setField(service, "attendanceRuleService", ruleService); |
| | | ReflectionTestUtils.setField(service, "attendanceRecordMapper", mapper); |
| | | LocalDateTime clockTime = LocalDateTime.of(2026, 10, 22, 18, 56); |
| | | when(holidayService.isWorkDay(any(LocalDate.class))).thenReturn(true); |
| | | HrmEmployeeDO employee = HrmEmployeeDO.builder().id(1L).deptId(2L).build(); |
| | | when(employeeService.getEmployee(1L)).thenReturn(employee); |
| | | when(ruleService.getAttendanceRuleByDeptId(2L)).thenReturn(requiredRule()); |
| | | assertThrows(ServiceException.class, |
| | | () -> service.clockOut(1L, clockTime, position("0.00091", "20"), null)); |
| | | verify(mapper, never()).insert(any(HrmAttendanceRecordDO.class)); |
| | | verify(mapper, never()).updateById(any(HrmAttendanceRecordDO.class)); |
| | | } |
| | | } |