package cn.iocoder.yudao.module.hrm.service.remind; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.module.hrm.dal.dataobject.certificate.HrmCertificateDO; import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeContractDO; import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO; import cn.iocoder.yudao.module.hrm.dal.mysql.certificate.HrmCertificateMapper; import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeContractMapper; import cn.iocoder.yudao.module.hrm.dal.redis.HrmExpiryRemindRedisDAO; import cn.iocoder.yudao.module.hrm.enums.HrmEmployeeStatusEnum; import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService; import cn.iocoder.yudao.module.infra.api.config.ConfigApi; import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi; import cn.iocoder.yudao.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.time.LocalDate; import java.util.HashMap; import java.util.List; import java.util.Map; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; /** * HRM 证书/合同到期提醒 Service 实现类 * *

参照 MES 计量器具到期提醒实现:Redis 日锁防多实例并发重复扫描、按业务记录+到期日去重, * 向员工系统账号(hrm_employee.userId)发送站内信;提醒天数从 infra_config 读取,默认 30 天。 * * @author 超级管理员 */ @Slf4j @Service public class HrmExpiryRemindServiceImpl implements HrmExpiryRemindService { /** * 业务标识:证书资质 */ private static final String BIZ_CERT = "cert"; /** * 业务标识:员工合同 */ private static final String BIZ_CONTRACT = "contract"; /** * 证书到期提醒天数配置 KEY(infra_config) */ private static final String CERT_REMIND_DAYS_KEY = "hrm.certificate.remind-days"; /** * 合同到期提醒天数配置 KEY(infra_config) */ private static final String CONTRACT_REMIND_DAYS_KEY = "hrm.contract.remind-days"; /** * 提醒天数默认值 */ private static final int DEFAULT_REMIND_DAYS = 30; /** * 证书到期提醒站内信模板编号 */ private static final String CERT_REMIND_TEMPLATE = "hrm_certificate_expire_remind"; /** * 合同到期提醒站内信模板编号 */ private static final String CONTRACT_REMIND_TEMPLATE = "hrm_contract_expire_remind"; @Resource private HrmCertificateMapper certificateMapper; @Resource private HrmEmployeeContractMapper contractMapper; @Resource private HrmExpiryRemindRedisDAO expiryRemindRedisDAO; @Resource private ConfigApi configApi; @Resource private NotifyMessageSendApi notifyMessageSendApi; @Resource private HrmEmployeeService employeeService; @Override public void sendAll() { sendCertificateExpireRemind(); sendContractExpireRemind(); } @Override public void sendCertificateExpireRemind() { try { LocalDate today = LocalDate.now(); if (!expiryRemindRedisDAO.tryAcquireScanLock(BIZ_CERT, today)) { return; } int remindDays = getRemindDays(CERT_REMIND_DAYS_KEY); List list = certificateMapper.selectListByExpireDateBetween( today, today.plusDays(remindDays)); if (CollUtil.isEmpty(list)) { return; } Map employeeMap = buildEmployeeMap( convertSet(list, HrmCertificateDO::getEmployeeId)); for (HrmCertificateDO certificate : list) { if (certificate.getExpireDate() == null) { continue; } HrmEmployeeDO employee = employeeMap.get(certificate.getEmployeeId()); if (!canNotify(employee)) { continue; } if (!expiryRemindRedisDAO.tryMarkReminded(BIZ_CERT, certificate.getId(), certificate.getExpireDate())) { continue; } sendCertificateRemind(certificate, employee); } } catch (Exception ex) { // best-effort:提醒失败不影响列表查询/定时任务整体 log.error("[sendCertificateExpireRemind][发送证书到期提醒失败]", ex); } } @Override public void sendContractExpireRemind() { try { LocalDate today = LocalDate.now(); if (!expiryRemindRedisDAO.tryAcquireScanLock(BIZ_CONTRACT, today)) { return; } int remindDays = getRemindDays(CONTRACT_REMIND_DAYS_KEY); List list = contractMapper.selectListExpiringBetween( today, today.plusDays(remindDays)); if (CollUtil.isEmpty(list)) { return; } Map employeeMap = buildEmployeeMap( convertSet(list, HrmEmployeeContractDO::getEmployeeId)); for (HrmEmployeeContractDO contract : list) { if (contract.getEndDate() == null) { continue; } HrmEmployeeDO employee = employeeMap.get(contract.getEmployeeId()); if (!canNotify(employee)) { continue; } if (!expiryRemindRedisDAO.tryMarkReminded(BIZ_CONTRACT, contract.getId(), contract.getEndDate())) { continue; } sendContractRemind(contract, employee); } } catch (Exception ex) { // best-effort:提醒失败不影响列表查询/定时任务整体 log.error("[sendContractExpireRemind][发送合同到期提醒失败]", ex); } } private boolean canNotify(HrmEmployeeDO employee) { // 仅向有系统账号且在岗/试用员工发送提醒;离职员工(账号多已停用)不打扰 return employee != null && employee.getUserId() != null && !HrmEmployeeStatusEnum.LEAVE.getStatus().equals(employee.getEmployeeStatus()); } private void sendCertificateRemind(HrmCertificateDO certificate, HrmEmployeeDO employee) { Map params = new HashMap<>(); params.put("employeeName", StrUtil.nullToEmpty(employee.getName())); params.put("certTypeName", StrUtil.nullToEmpty(convertCertTypeName(certificate.getCertType()))); params.put("certName", StrUtil.nullToEmpty(certificate.getCertName())); params.put("expireDate", StrUtil.nullToEmpty(certificate.getExpireDate() == null ? null : certificate.getExpireDate().toString())); notifyMessageSendApi.sendSingleMessageToAdmin(new NotifySendSingleToUserReqDTO() .setUserId(employee.getUserId()) .setTemplateCode(CERT_REMIND_TEMPLATE) .setTemplateParams(params)); } private void sendContractRemind(HrmEmployeeContractDO contract, HrmEmployeeDO employee) { Map params = new HashMap<>(); params.put("employeeName", StrUtil.nullToEmpty(employee.getName())); params.put("contractNo", StrUtil.nullToEmpty(contract.getContractNo())); params.put("endDate", StrUtil.nullToEmpty(contract.getEndDate() == null ? null : contract.getEndDate().toString())); notifyMessageSendApi.sendSingleMessageToAdmin(new NotifySendSingleToUserReqDTO() .setUserId(employee.getUserId()) .setTemplateCode(CONTRACT_REMIND_TEMPLATE) .setTemplateParams(params)); } private Map buildEmployeeMap(java.util.Collection employeeIds) { if (CollUtil.isEmpty(employeeIds)) { return Map.of(); } return employeeService.getEmployeeList(employeeIds).stream() .collect(java.util.stream.Collectors.toMap(HrmEmployeeDO::getId, e -> e, (a, b) -> a)); } private int getRemindDays(String configKey) { String value = configApi.getConfigValueByKey(configKey); if (StrUtil.isBlank(value)) { return DEFAULT_REMIND_DAYS; } try { return Integer.parseInt(value.trim()); } catch (NumberFormatException ex) { log.warn("[getRemindDays][配置 {} 的值 {} 非法,使用默认值 {}]", configKey, value, DEFAULT_REMIND_DAYS); return DEFAULT_REMIND_DAYS; } } private String convertCertTypeName(Integer certType) { return switch (certType == null ? 0 : certType) { case 1 -> "特种作业证书"; case 2 -> "从业资质"; case 3 -> "技能等级证书"; case 4 -> "安全培训合格证"; default -> "证书"; }; } }