tuqin
6 小时以前 52feee3e55403d2858da59605173ec055f601bfa
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceRecordController.java
@@ -2,13 +2,17 @@
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
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.controller.admin.attendance.vo.HrmAttendanceMyStatusRespVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceRecordDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.service.attendance.HrmAttendanceRecordService;
@@ -28,6 +32,7 @@
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
@@ -54,12 +59,13 @@
    @PostMapping("/clock-in")
    @Operation(summary = "上班打卡")
    @PreAuthorize("@ss.hasPermission('hrm:attendance:clock')")
    @PreAuthorize("@ss.hasPermission('hrm:attendance-record:clock')")
    public CommonResult<HrmAttendanceRecordRespVO> clockIn(@RequestParam("location") String location) {
        Long loginUserId = getLoginUserId();
        HrmEmployeeDO employee = employeeService.getEmployeeByUserId(loginUserId);
        if (employee == null) {
            throw exception(EMPLOYEE_NOT_EXISTS, "当前用户未关联员工信息,无法打卡");
            throw new ServiceException(EMPLOYEE_NOT_EXISTS.getCode(),
                    "当前登录账号未关联员工信息,无法打卡。请联系管理员在【员工管理】中将该登录账号绑定到员工。");
        }
        HrmAttendanceRecordDO record = attendanceRecordService.clockIn(employee.getId(), LocalDateTime.now(), location);
        HrmAttendanceRecordRespVO respVO = BeanUtils.toBean(record, HrmAttendanceRecordRespVO.class);
@@ -75,12 +81,13 @@
    @PostMapping("/clock-out")
    @Operation(summary = "下班打卡")
    @PreAuthorize("@ss.hasPermission('hrm:attendance:clock')")
    @PreAuthorize("@ss.hasPermission('hrm:attendance-record:clock')")
    public CommonResult<HrmAttendanceRecordRespVO> clockOut(@RequestParam("location") String location) {
        Long loginUserId = getLoginUserId();
        HrmEmployeeDO employee = employeeService.getEmployeeByUserId(loginUserId);
        if (employee == null) {
            throw exception(EMPLOYEE_NOT_EXISTS, "当前用户未关联员工信息,无法打卡");
            throw new ServiceException(EMPLOYEE_NOT_EXISTS.getCode(),
                    "当前登录账号未关联员工信息,无法打卡。请联系管理员在【员工管理】中将该登录账号绑定到员工。");
        }
        HrmAttendanceRecordDO record = attendanceRecordService.clockOut(employee.getId(), LocalDateTime.now(), location);
        HrmAttendanceRecordRespVO respVO = BeanUtils.toBean(record, HrmAttendanceRecordRespVO.class);
@@ -94,13 +101,21 @@
        return success(respVO);
    }
    private Long getLoginUserId() {
        return cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId();
    @GetMapping("/my-status")
    @Operation(summary = "获得当前登录人的考勤状态", description = "App 打卡页使用:返回当日考勤规则、打卡记录与可打卡状态")
    @Parameter(name = "date", description = "考勤日期,格式 yyyy-MM-dd,默认服务器当天", example = "2026-10-17")
    public CommonResult<HrmAttendanceMyStatusRespVO> getMyStatus(
            @RequestParam(value = "date", required = false)
            @org.springframework.format.annotation.DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
        return success(attendanceRecordService.getMyStatus(getLoginUserId(), date));
    }
    private Long getLoginUserId() {
        return cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId();    }
    @GetMapping("/page")
    @Operation(summary = "获得考勤记录分页")
    @PreAuthorize("@ss.hasPermission('hrm:attendance:query')")
    @PreAuthorize("@ss.hasPermission('hrm:attendance-record:query')")
    public CommonResult<PageResult<HrmAttendanceRecordRespVO>> getAttendanceRecordPage(@Valid HrmAttendanceRecordPageReqVO pageReqVO) {
        PageResult<HrmAttendanceRecordDO> pageResult = attendanceRecordService.getAttendanceRecordPage(pageReqVO);
        // 转换为 VO
@@ -142,6 +157,7 @@
            HrmEmployeeDO employee = employeeMap.get(respVO.getUserId());
            if (employee != null) {
                respVO.setUserName(employee.getName());
                respVO.setPhone(employee.getPhone());
            }
            // 部门名称
            DeptRespDTO dept = deptMap.get(respVO.getDeptId());
@@ -154,7 +170,7 @@
    @GetMapping("/get")
    @Operation(summary = "获得考勤记录")
    @Parameter(name = "id", description = "记录ID", required = true)
    @PreAuthorize("@ss.hasPermission('hrm:attendance:query')")
    @PreAuthorize("@ss.hasPermission('hrm:attendance-record:query')")
    public CommonResult<HrmAttendanceRecordRespVO> getAttendanceRecord(@RequestParam("id") Long id) {
        HrmAttendanceRecordDO attendanceRecord = attendanceRecordService.getAttendanceRecord(id);
        if (attendanceRecord == null) {
@@ -166,6 +182,7 @@
            HrmEmployeeDO employee = employeeService.getEmployee(attendanceRecord.getUserId());
            if (employee != null) {
                respVO.setUserName(employee.getName());
                respVO.setPhone(employee.getPhone());
            }
        }
        if (attendanceRecord.getDeptId() != null) {
@@ -177,9 +194,107 @@
        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')")
    @PreAuthorize("@ss.hasPermission('hrm:attendance-record:export')")
    @ApiAccessLog(operateType = EXPORT)
    public void exportAttendanceRecordExcel(@Valid HrmAttendanceRecordPageReqVO pageReqVO,
                                              HttpServletResponse response) throws IOException {