package cn.iocoder.yudao.module.mes.service.safety;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.hrm.api.employee.HrmEmployeeApi;
import cn.iocoder.yudao.module.hrm.api.employee.dto.HrmEmployeeSimpleDTO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.cert.SafetyCertificatePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.cert.SafetyCertificateRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.safety.vo.cert.SafetyCertificateSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyCertificateDO;
import cn.iocoder.yudao.module.mes.dal.mysql.safety.SafetyCertificateMapper;
import cn.iocoder.yudao.module.mes.enums.safety.SafetyCertificateStatusEnum;
import cn.iocoder.yudao.module.mes.enums.safety.SafetyCertificateTypeEnum;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.SafetyErrorCodeConstants.*;
/**
* 安全生产 - 特种作业证台账 Service
*
*
核心能力:证书到期自动预警。状态由 {@link #resolveStatus} 根据有效期实时推导,
* 不依赖手工维护,避免过期未及时更新状态。
*
* 持证人通过 employeeId 关联人力资源的员工档案:
* 新增/修改时从档案带出姓名、工号、部门并落为快照;查询时实时回填最新档案信息,
* 员工调岗后证件列表也会跟着显示新部门。
*
* @author 超级管理员
*/
@Service
@Validated
public class SafetyCertificateService {
/** 默认提前预警天数 */
public static final int DEFAULT_WARN_DAYS = 30;
@Resource
private SafetyCertificateMapper certificateMapper;
@Resource
private HrmEmployeeApi hrmEmployeeApi;
@Resource
private SafetyRefResolver refResolver;
public Long createCertificate(SafetyCertificateSaveReqVO reqVO) {
reqVO.setId(null);
validateCertificateForCreate(reqVO);
SafetyCertificateDO cert = buildCertificate(reqVO, null);
certificateMapper.insert(cert);
return cert.getId();
}
public void updateCertificate(SafetyCertificateSaveReqVO reqVO) {
SafetyCertificateDO old = validateCertificateExists(reqVO.getId());
validateCertificateForUpdate(reqVO, old);
SafetyCertificateDO cert = buildCertificate(reqVO, old);
certificateMapper.updateById(cert);
}
public void deleteCertificate(Long id) {
validateCertificateExists(id);
certificateMapper.deleteById(id);
}
public SafetyCertificateDO getCertificate(Long id) {
return validateCertificateExists(id);
}
public PageResult getCertificatePage(SafetyCertificatePageReqVO reqVO) {
return certificateMapper.selectPage(reqVO);
}
/**
* 查询即将到期 / 已过期的证件列表
*
* @param days 提前预警天数,为空使用默认 30 天
*/
public List getExpiringCertificates(Integer days) {
int warnDays = days == null || days <= 0 ? DEFAULT_WARN_DAYS : days;
return certificateMapper.selectExpiringBefore(LocalDate.now().plusDays(warnDays));
}
/**
* 批量补全持证人的最新员工档案信息(姓名、工号、部门、手机、关联用户)
*
* 查询时用档案实时值覆盖快照值,保证员工调岗后台账同步;
* 员工档案已被删除时回退到快照值,保证历史证件仍可展示。
*/
public void fillEmployeeInfo(List certs) {
List employeeIds = certs.stream()
.map(SafetyCertificateDO::getEmployeeId)
.filter(Objects::nonNull).distinct().toList();
// 用 new HashMap<>() 而非 Map.of():employeeId 允许为 null,
// 后面 employeeMap.get(cert.getEmployeeId()) 在空集合场景会 get(null),
// Map.of() 的 get(null) 会抛 NPE 导致证件列表接口 500。
Map employeeMap = employeeIds.isEmpty() ? new HashMap<>()
: hrmEmployeeApi.getEmployeeList(employeeIds).stream()
.collect(Collectors.toMap(HrmEmployeeSimpleDTO::getId, e -> e, (a, b) -> a));
certs.forEach(cert -> {
HrmEmployeeSimpleDTO employee = employeeMap.get(cert.getEmployeeId());
if (employee != null) {
cert.setEmployeeName(employee.getName());
cert.setEmployeeNo(employee.getEmployeeNo());
cert.setDeptName(employee.getDeptName());
}
});
}
/**
* 批量补全展示字段,持证人档案一次性查好,避免逐条查询
*/
public void fillDisplayFieldsBatch(List certs, List resps) {
var employeeMap = refResolver.employeeMap(certs.stream().map(SafetyCertificateDO::getEmployeeId).toList());
for (int i = 0; i < certs.size(); i++) {
fillDisplayFields(certs.get(i), resps.get(i), employeeMap.get(certs.get(i).getEmployeeId()));
}
}
/**
* 单个证件的展示字段补全(类型名称、状态名称、剩余天数、持证人档案)
*/
public void fillDisplayFields(SafetyCertificateDO cert, SafetyCertificateRespVO respVO) {
fillDisplayFields(cert, respVO, cert.getEmployeeId() == null ? null
: hrmEmployeeApi.getEmployee(cert.getEmployeeId()));
}
private void fillDisplayFields(SafetyCertificateDO cert, SafetyCertificateRespVO respVO,
HrmEmployeeSimpleDTO employee) {
respVO.setCertTypeName(SafetyCertificateTypeEnum.getNameByType(cert.getCertType()));
String status = resolveStatus(cert.getExpireDate(), cert.getWarnDays(), cert.getStatus());
respVO.setStatus(status);
respVO.setStatusName(SafetyCertificateStatusEnum.getNameByStatus(status));
if (cert.getExpireDate() != null) {
respVO.setRemainingDays(ChronoUnit.DAYS.between(LocalDate.now(), cert.getExpireDate()));
}
if (employee != null) {
respVO.setEmployeeName(employee.getName());
respVO.setEmployeeNo(employee.getEmployeeNo());
respVO.setDeptName(employee.getDeptName());
respVO.setPhone(employee.getPhone());
respVO.setUserId(employee.getUserId());
}
}
/**
* 根据有效期推导状态:已注销保持不变;过期 → EXPIRED;在预警期内 → EXPIRING;否则 VALID
*
* @param expireDate 有效期
* @param warnDays 提前预警天数
* @param current 当前状态,用于保护 REVOKED
*/
public static String resolveStatus(LocalDate expireDate, Integer warnDays, String current) {
if (SafetyCertificateStatusEnum.REVOKED.getStatus().equals(current)) {
return SafetyCertificateStatusEnum.REVOKED.getStatus();
}
if (expireDate == null) {
return SafetyCertificateStatusEnum.VALID.getStatus();
}
LocalDate today = LocalDate.now();
if (expireDate.isBefore(today)) {
return SafetyCertificateStatusEnum.EXPIRED.getStatus();
}
int days = warnDays == null || warnDays <= 0 ? DEFAULT_WARN_DAYS : warnDays;
if (!expireDate.isAfter(today.plusDays(days))) {
return SafetyCertificateStatusEnum.EXPIRING.getStatus();
}
return SafetyCertificateStatusEnum.VALID.getStatus();
}
/**
* 构建 DO:持证人姓名/工号/部门从员工档案带出并落为快照
*/
private SafetyCertificateDO buildCertificate(SafetyCertificateSaveReqVO reqVO, SafetyCertificateDO old) {
HrmEmployeeSimpleDTO employee = hrmEmployeeApi.getEmployee(reqVO.getEmployeeId());
if (employee == null) {
throw exception(SAFETY_CERT_EMPLOYEE_NOT_EXISTS);
}
SafetyCertificateDO cert = BeanUtils.toBean(reqVO, SafetyCertificateDO.class);
cert.setEmployeeId(employee.getId());
cert.setEmployeeName(employee.getName());
cert.setEmployeeNo(employee.getEmployeeNo());
cert.setDeptName(employee.getDeptName());
cert.setCertNo(StrUtil.trim(reqVO.getCertNo()));
cert.setWarnDays(reqVO.getWarnDays() == null ? DEFAULT_WARN_DAYS : reqVO.getWarnDays());
// 已注销状态不允许通过编辑改回
String currentStatus = old == null ? reqVO.getStatus() : old.getStatus();
cert.setStatus(resolveStatus(reqVO.getExpireDate(), cert.getWarnDays(), currentStatus));
cert.setRemark(reqVO.getRemark() == null ? "" : reqVO.getRemark().trim());
return cert;
}
private void validateCertificateForCreate(SafetyCertificateSaveReqVO reqVO) {
validateExpireAfterIssue(reqVO);
if (certificateMapper.selectByCertNo(StrUtil.trim(reqVO.getCertNo())) != null) {
throw exception(SAFETY_CERT_NO_DUPLICATE);
}
}
private void validateCertificateForUpdate(SafetyCertificateSaveReqVO reqVO, SafetyCertificateDO old) {
validateExpireAfterIssue(reqVO);
SafetyCertificateDO byCertNo = certificateMapper.selectByCertNo(StrUtil.trim(reqVO.getCertNo()));
if (byCertNo != null && !byCertNo.getId().equals(old.getId())) {
throw exception(SAFETY_CERT_NO_DUPLICATE);
}
}
private void validateExpireAfterIssue(SafetyCertificateSaveReqVO reqVO) {
if (reqVO.getIssueDate() != null && reqVO.getExpireDate() != null
&& !Objects.requireNonNull(reqVO.getExpireDate()).isAfter(reqVO.getIssueDate())) {
throw exception(SAFETY_CERT_EXPIRE_BEFORE_ISSUE);
}
}
private SafetyCertificateDO validateCertificateExists(Long id) {
SafetyCertificateDO cert = id == null ? null : certificateMapper.selectById(id);
if (cert == null) {
throw exception(SAFETY_CERT_NOT_EXISTS);
}
return cert;
}
}