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; } }