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 -> "证书";
};
}
}