gaoluyang
5 小时以前 1cbc592d8b674b63a6e2117d7c48ab4ff8cb867e
fix(hrm): 考勤异常申请字段匹配与申请人回填;补充 hrm_audit_status 字典脚本

- 新增时按当前登录账号回填申请人与部门,未关联员工档案时给出明确提示
- 分页查询支持异常日期区间,修复前端日期范围筛选不生效
- 补充 hrm_audit_status 字典初始化脚本

Co-Authored-By: Claude Code <noreply@anthropic.com>
已添加1个文件
已修改4个文件
69 ■■■■■ 文件已修改
docs/sql/hrm_audit_status_dict.sql 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceExceptionPageReqVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/attendance/HrmAttendanceExceptionMapper.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceExceptionServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/hrm_audit_status_dict.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
-- HRM å®¡æ‰¹çŠ¶æ€å­—å…¸
-- å½±å“é¡µé¢ï¼šäººåŠ›èµ„æºæ¨¡å—ä¸­æ‰€æœ‰ä½¿ç”¨ hrm_audit_status å­—典的单据列表/详情(考勤异常申请、请假、离职、调岗等)
--
-- èƒŒæ™¯ï¼šä»£ç ä¸­å¤§é‡ä½¿ç”¨ @DictFormat("hrm_audit_status") ä¸Žå‰ç«¯ DICT_TYPE.HRM_AUDIT_STATUS,
--       ä½† system_dict_type / system_dict_data ä¸­ç¼ºå°‘该字典,导致状态列显示为空、导出无中文名。
--
-- æœ¬è„šæœ¬å¹‚ç­‰ï¼Œå¯é‡å¤æ‰§è¡Œã€‚æ‰§è¡ŒåŽæ¸…ç†å­—å…¸ç¼“å­˜ï¼ˆé‡å¯åŽç«¯ï¼Œæˆ–åˆ é™¤ Redis ä¸­çš„ dict_data:* ç›¸å…³é”®ï¼‰ï¼Œ
-- å‰ç«¯é‡æ–°ç™»å½•/刷新后生效。
-- 1. å­—典类型
INSERT INTO system_dict_type (name, type, status, remark, creator, updater)
SELECT 'HRM å®¡æ‰¹çŠ¶æ€', 'hrm_audit_status', 0, '人力模块单据审批状态', '1', '1'
WHERE NOT EXISTS (
    SELECT 1 FROM system_dict_type WHERE type = 'hrm_audit_status' AND deleted = b'0'
);
-- 2. å­—典数据(与 HrmAuditStatusEnum ä¿æŒä¸€è‡´ï¼‰
INSERT INTO system_dict_data (sort, label, value, dict_type, status, color_type, css_class, remark, creator, updater)
SELECT s.sort, s.label, s.value, 'hrm_audit_status', 0, s.color_type, '', '', '1', '1'
FROM (
    SELECT 0 AS sort, '草稿' AS label, '0' AS value, 'info' AS color_type
    UNION ALL SELECT 10, '审批中', '10', 'default'
    UNION ALL SELECT 20, '审批通过', '20', 'success'
    UNION ALL SELECT 30, '审批驳回', '30', 'danger'
    UNION ALL SELECT 40, '已撤销', '40', 'warning'
) s
WHERE NOT EXISTS (
    SELECT 1 FROM system_dict_data d
    WHERE d.dict_type = 'hrm_audit_status' AND d.value = s.value AND d.deleted = b'0'
);
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/enums/ErrorCodeConstants.java
@@ -93,6 +93,7 @@
    ErrorCode ATTENDANCE_EXCEPTION_UPDATE_AUDIT_STATUS_FAIL = new ErrorCode(1_030_004_014, "更新考勤异常审批状态失败,原因:不是审批中状态");
    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_EXCEPTION_EMPLOYEE_NOT_EXISTS = new ErrorCode(1_030_004_018, "新增考勤异常申请失败,当前登录账号({})未关联员工档案,无法确定申请人和部门。请使用已关联员工档案的账号登录,或先在「员工档案」中维护当前账号后再提交");
    // ========== è–ªé…¬ç®¡ç† 1-030-005-000 ==========
    ErrorCode SALARY_STRUCTURE_NOT_EXISTS = new ErrorCode(1_030_005_000, "薪酬结构不存在");
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceExceptionPageReqVO.java
@@ -6,6 +6,8 @@
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.time.LocalDate;
@Schema(description = "管理后台 - è€ƒå‹¤å¼‚常申请分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@@ -27,4 +29,7 @@
    @Schema(description = "状态", example = "0")
    private Integer status;
    @Schema(description = "异常日期区间,传 [开始日期, ç»“束日期]", example = "[\"2026-01-01\", \"2026-12-31\"]")
    private LocalDate[] date;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/mysql/attendance/HrmAttendanceExceptionMapper.java
@@ -7,6 +7,8 @@
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceExceptionDO;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDate;
/**
 * è€ƒå‹¤å¼‚常申请 Mapper
 *
@@ -16,13 +18,18 @@
public interface HrmAttendanceExceptionMapper extends BaseMapperX<HrmAttendanceExceptionDO> {
    default PageResult<HrmAttendanceExceptionDO> selectPage(HrmAttendanceExceptionPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<HrmAttendanceExceptionDO>()
        LambdaQueryWrapperX<HrmAttendanceExceptionDO> query = new LambdaQueryWrapperX<HrmAttendanceExceptionDO>()
                .likeIfPresent(HrmAttendanceExceptionDO::getNo, reqVO.getNo())
                .eqIfPresent(HrmAttendanceExceptionDO::getUserId, reqVO.getUserId())
                .eqIfPresent(HrmAttendanceExceptionDO::getDeptId, reqVO.getDeptId())
                .eqIfPresent(HrmAttendanceExceptionDO::getType, reqVO.getType())
                .eqIfPresent(HrmAttendanceExceptionDO::getStatus, reqVO.getStatus())
                .orderByDesc(HrmAttendanceExceptionDO::getId));
                .eqIfPresent(HrmAttendanceExceptionDO::getStatus, reqVO.getStatus());
        // å¼‚常日期区间筛选
        LocalDate[] dateRange = reqVO.getDate();
        if (dateRange != null && dateRange.length >= 2 && dateRange[0] != null && dateRange[1] != null) {
            query.between(HrmAttendanceExceptionDO::getExceptionDate, dateRange[0], dateRange[1]);
        }
        return selectPage(reqVO, query.orderByDesc(HrmAttendanceExceptionDO::getId));
    }
    default HrmAttendanceExceptionDO selectByNo(String no) {
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceExceptionServiceImpl.java
@@ -8,8 +8,12 @@
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceExceptionPageReqVO;
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceExceptionSaveReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceExceptionDO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmAttendanceExceptionMapper;
import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO;
import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum;
import jakarta.annotation.Resource;
@@ -36,6 +40,10 @@
    @Resource
    private HrmAttendanceExceptionMapper attendanceExceptionMapper;
    @Resource
    private HrmEmployeeService employeeService;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private HrmNoRedisDAO noRedisDAO;
    @Resource
    private BpmProcessInstanceApi bpmProcessInstanceApi;
@@ -48,6 +56,18 @@
    public Long createAttendanceException(HrmAttendanceExceptionSaveReqVO createReqVO) {
        HrmAttendanceExceptionDO attendanceException = BeanUtils.toBean(createReqVO, HrmAttendanceExceptionDO.class);
        attendanceException.setNo(generateNo());
        // ç”³è¯·äººé»˜è®¤ä¸ºå½“前登录用户对应的员工,前端表单不选择申请人
        Long loginUserId = cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId();
        HrmEmployeeDO employee = employeeService.getEmployeeByUserId(loginUserId);
        if (employee == null) {
            AdminUserRespDTO loginUser = adminUserApi.getUser(loginUserId);
            throw exception(ATTENDANCE_EXCEPTION_EMPLOYEE_NOT_EXISTS,
                    loginUser != null ? loginUser.getNickname() : String.valueOf(loginUserId));
        }
        attendanceException.setUserId(employee.getId());
        if (attendanceException.getDeptId() == null) {
            attendanceException.setDeptId(employee.getDeptId());
        }
        attendanceException.setStatus(HrmAuditStatusEnum.DRAFT.getStatus());
        attendanceExceptionMapper.insert(attendanceException);
        storageAttachmentApi.bindAttachments("file", "hrm_attendance_exception",