yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceRecordController.java
@@ -7,6 +7,8 @@ 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.HrmAttendanceRecordImportExcelVO; import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordImportReqVO; import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordPageReqVO; import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceRecordRespVO; import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRecordDO; @@ -142,6 +144,7 @@ HrmEmployeeDO employee = employeeMap.get(respVO.getUserId()); if (employee != null) { respVO.setUserName(employee.getName()); respVO.setPhone(employee.getPhone()); } // é¨é¨åç§° DeptRespDTO dept = deptMap.get(respVO.getDeptId()); @@ -166,6 +169,7 @@ HrmEmployeeDO employee = employeeService.getEmployee(attendanceRecord.getUserId()); if (employee != null) { respVO.setUserName(employee.getName()); respVO.setPhone(employee.getPhone()); } } if (attendanceRecord.getDeptId() != null) { @@ -177,6 +181,104 @@ return success(respVO); } @GetMapping("/get-import-template") @Operation(summary = "ä¸è½½èå¤è®°å½å¯¼å ¥æ¨¡æ¿") @PreAuthorize("@ss.hasPermission('hrm:attendance-record:import')") public void importTemplate(HttpServletResponse response) throws IOException { List<HrmAttendanceRecordImportExcelVO> list = Collections.singletonList( HrmAttendanceRecordImportExcelVO.builder() .userName("å¼ ä¸") .phone("13800138000") .date(java.time.LocalDate.of(2026, 1, 15)) .clockInTime(java.time.LocalDateTime.of(2026, 1, 15, 8, 30)) .clockOutTime(java.time.LocalDateTime.of(2026, 1, 15, 17, 30)) .clockInType(1) .clockOutType(1) .location("åå ¬åº") .remark("ç¤ºä¾æ°æ®") .build() ); ExcelUtils.write(response, "èå¤å¯¼å ¥æ¨¡æ¿.xls", "èå¤è®°å½", HrmAttendanceRecordImportExcelVO.class, list); } @PostMapping("/import") @Operation(summary = "å¯¼å ¥èå¤è®°å½") @PreAuthorize("@ss.hasPermission('hrm:attendance-record:import')") public CommonResult<String> importExcel(@Valid HrmAttendanceRecordImportReqVO importReqVO) throws IOException { List<HrmAttendanceRecordImportExcelVO> list = ExcelUtils.read(importReqVO.getFile(), HrmAttendanceRecordImportExcelVO.class); if (CollUtil.isEmpty(list)) { return success("没æå¯å¯¼å ¥çæ°æ®"); } int successCount = 0; Set<String> missingNames = new LinkedHashSet<>(); Set<String> duplicateNames = new LinkedHashSet<>(); Set<String> invalidRows = new LinkedHashSet<>(); for (HrmAttendanceRecordImportExcelVO item : list) { if (item.getDate() == null) { invalidRows.add((item.getUserName() == null || item.getUserName().isBlank()) ? "æªå¡«å§åçè¡" : "è夿¥æä¸ºç©º(" + item.getUserName().trim() + ")"); continue; } if (item.getUserName() == null || item.getUserName().isBlank()) { invalidRows.add("è夿¥æä¸º " + item.getDate() + " 使ªå¡«åå·¥å§å"); continue; } List<HrmEmployeeDO> employees = employeeService.getEmployeeListByName(item.getUserName().trim()); if (employees.isEmpty()) { missingNames.add(item.getUserName().trim()); continue; } HrmEmployeeDO employee = employees.get(0); // åå¨å¤ä¸ªåååå·¥æ¶ï¼ç¨ææºå·åºå if (employees.size() > 1) { String phone = item.getPhone() != null ? item.getPhone().trim() : null; List<HrmEmployeeDO> matchedByPhone = new ArrayList<>(); if (phone != null && !phone.isEmpty()) { for (HrmEmployeeDO emp : employees) { if (phone.equals(emp.getPhone())) { matchedByPhone.add(emp); } } } if (matchedByPhone.size() == 1) { employee = matchedByPhone.get(0); } else { duplicateNames.add(item.getUserName().trim()); continue; } } HrmAttendanceRecordDO record = attendanceRecordService.getAttendanceRecordByUserIdAndDate(employee.getId(), item.getDate()); if (record == null) { record = HrmAttendanceRecordDO.builder() .userId(employee.getId()).deptId(employee.getDeptId()).date(item.getDate()) .clockInTime(item.getClockInTime()).clockOutTime(item.getClockOutTime()) .clockInType(item.getClockInType()).clockOutType(item.getClockOutType()) .location(item.getLocation()).remark(item.getRemark()) .dataSource(2).build(); attendanceRecordService.createImportedRecord(record); } else if (Boolean.TRUE.equals(importReqVO.getUpdateSupport())) { record.setClockInTime(item.getClockInTime()); record.setClockOutTime(item.getClockOutTime()); record.setClockInType(item.getClockInType()); record.setClockOutType(item.getClockOutType()); record.setLocation(item.getLocation()); record.setRemark(item.getRemark()); attendanceRecordService.updateImportedRecord(record); } successCount++; } StringBuilder msg = new StringBuilder("æåå¯¼å ¥ " + successCount + " æ¡"); if (!missingNames.isEmpty()) { msg.append("ï¼ä»¥ä¸åå·¥ä¸åå¨ï¼æªè½å¯¼å ¥ï¼").append(String.join("ã", missingNames)); } if (!duplicateNames.isEmpty()) { msg.append("ï¼ä»¥ä¸åå·¥åå¨å¤ååååå·¥ï¼éè¡¥å ææºå·åºåï¼æªè½å¯¼å ¥ï¼").append(String.join("ã", duplicateNames)); } if (!invalidRows.isEmpty()) { msg.append("ï¼ä»¥ä¸è®°å½æ°æ®ä¸å®æ´ï¼æªè½å¯¼å ¥ï¼").append(String.join("ã", invalidRows)); } return success(msg.toString()); } @GetMapping("/export-excel") @Operation(summary = "导åºèå¤è®°å½ Excel") @PreAuthorize("@ss.hasPermission('hrm:attendance:export')") yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRecordImportExcelVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,51 @@ package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo; import cn.idev.excel.annotation.ExcelProperty; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.time.LocalDate; import java.time.LocalDateTime; /** * èå¤è®°å½ Excel å¯¼å ¥æ¨¡æ¿ VO */ @Data @Builder @AllArgsConstructor @NoArgsConstructor public class HrmAttendanceRecordImportExcelVO { @ExcelProperty("åå·¥å§å") private String userName; @ExcelProperty("ææºå·") private String phone; @ExcelProperty("è夿¥æ") private LocalDate date; @ExcelProperty("ä¸çæå¡æ¶é´") private LocalDateTime clockInTime; @ExcelProperty("ä¸çæå¡æ¶é´") private LocalDateTime clockOutTime; @ExcelProperty(value = "ä¸çæå¡ç±»å", converter = DictConvert.class) @DictFormat("hrm_clock_type") private Integer clockInType; @ExcelProperty(value = "ä¸çæå¡ç±»å", converter = DictConvert.class) @DictFormat("hrm_clock_type") private Integer clockOutType; @ExcelProperty("æå¡å°ç¹") private String location; @ExcelProperty("夿³¨") private String remark; } yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRecordImportReqVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,18 @@ package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; import org.springframework.web.multipart.MultipartFile; @Schema(description = "管çåå° - èå¤è®°å½å¯¼å ¥ Request VO") @Data public class HrmAttendanceRecordImportReqVO { @Schema(description = "Excel æä»¶", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "Excel æä»¶ä¸è½ä¸ºç©º") private MultipartFile file; @Schema(description = "æ¯å¦è¦çå·²åå¨çæ°æ®") private Boolean updateSupport = false; } yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceRecordRespVO.java
@@ -34,6 +34,10 @@ @ExcelProperty("é¨é¨åç§°") private String deptName; @Schema(description = "ææºå·") @ExcelProperty("ææºå·") private String phone; @Schema(description = "è夿¥æ") @ExcelProperty("è夿¥æ") private LocalDate date; yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/employee/HrmEmployeeMapper.java
@@ -21,6 +21,14 @@ return selectOne(HrmEmployeeDO::getPhone, phone); } default HrmEmployeeDO selectByName(String name) { return selectOne(HrmEmployeeDO::getName, name); } default List<HrmEmployeeDO> selectListByName(String name) { return selectList(HrmEmployeeDO::getName, name); } default HrmEmployeeDO selectByEmployeeNo(String employeeNo) { return selectOne(HrmEmployeeDO::getEmployeeNo, employeeNo); } yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordService.java
@@ -37,6 +37,12 @@ */ HrmAttendanceRecordDO getAttendanceRecordByUserIdAndDate(Long userId, LocalDate date); /** åå»ºå¯¼å ¥çèå¤è®°å½ */ void createImportedRecord(HrmAttendanceRecordDO record); /** æ´æ°å¯¼å ¥çèå¤è®°å½ */ void updateImportedRecord(HrmAttendanceRecordDO record); /** * æå¡ï¼ä¸çæå¡ï¼ * yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceRecordServiceImpl.java
@@ -67,6 +67,86 @@ } @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); } /** * æèå¤è§åéç®ä¸ä¸çæå¡ç±»åï¼æ£å¸¸/è¿å°/æ©éï¼ï¼ä¸æå¡é»è¾ä¿æä¸è´ */ 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(); yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeService.java
@@ -99,6 +99,12 @@ */ HrmEmployeeDO getEmployeeByUserId(Long userId); /** æ ¹æ®åå·¥å§åè·ååå·¥ */ HrmEmployeeDO getEmployeeByName(String name); /** æ ¹æ®åå·¥å§åè·åææåååå·¥ */ List<HrmEmployeeDO> getEmployeeListByName(String name); /** * å¯¼å ¥åå·¥å表 * yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/employee/HrmEmployeeServiceImpl.java
@@ -335,6 +335,16 @@ } @Override public HrmEmployeeDO getEmployeeByName(String name) { return employeeMapper.selectByName(name); } @Override public List<HrmEmployeeDO> getEmployeeListByName(String name) { return employeeMapper.selectListByName(name); } @Override public HrmEmployeeImportRespVO importEmployeeList(List<HrmEmployeeImportExcelVO> importEmployees, HrmEmployeeImportReqVO importReqVO) { // 1. æ ¡éªé空