xiaoyi
2026-10-29 81c76ca9e4741b71747b16eec61d0a00bec9f1f3
fix(safety): 特种作业证持证人改为关联员工档案,不再手填姓名

问题:原设计把持证人做成纯文本 employee_name/employee_no/dept_name,
与人力资源的员工档案、系统用户完全脱节,人名靠手打,无法核对也无法联动。

改动:
- hrm-api 新增 HrmEmployeeApi + HrmEmployeeSimpleDTO,供跨模块查询员工精简信息
(姓名/工号/部门/手机/关联 userId),实现类在 hrm 模块,部门名走 DeptApi 批量取
- safety_certificate 新增 employee_id 关联 hrm_employee.id(带索引),
原三列改为「快照」语义:新增/修改时从档案带出,查询时用档案最新值覆盖
- SafetyCertificateSaveReqVO: employeeName/employeeNo/deptName 移除,改为 employeeId 必填
- SafetyCertificateRespVO: 增加 employeeId/userId/phone
- 新增错误码 SAFETY_CERT_EMPLOYEE_NOT_EXISTS(持证人不存在)
- 台账导出改为用档案最新姓名/工号/部门

效果:
- 持证人只能从员工档案选,不再手填
- 员工调岗后台账部门自动跟着变(已验证:营销部 → 仓储物流部)
- 通过 hrm_employee.user_id 可继续追到系统用户账号

SQL:sql/mysql/safety_certificate_employee_20261029.sql(幂等,保留快照列不删)
已添加4个文件
已修改9个文件
363 ■■■■■ 文件已修改
sql/mysql/safety_certificate_employee_20261029.sql 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/api/employee/HrmEmployeeApi.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/api/employee/dto/HrmEmployeeSimpleDTO.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/api/employee/HrmEmployeeApiImpl.java 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/pom.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/SafetyCertificateController.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/cert/SafetyCertificatePageReqVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/cert/SafetyCertificateRespVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/cert/SafetyCertificateSaveReqVO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/SafetyCertificateDO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/SafetyErrorCodeConstants.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyCertificateService.java 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyLedgerExportService.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/safety_certificate_employee_20261029.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
-- ============================================================
-- å®‰å…¨ç”Ÿäº§ - ç‰¹ç§ä½œä¸šè¯æŒè¯äººæ”¹ä¸ºå…³è”员工档案
-- ç”Ÿæˆæ—¶é—´ï¼š2026-10-29
-- èƒŒæ™¯ï¼šåŽŸè®¾è®¡æŠŠæŒè¯äººåšæˆçº¯æ–‡æœ¬å§“åï¼ˆemployee_name / employee_no / dept_name),
--       æ— æ³•与人力资源的员工档案、系统用户打通。
-- åšæ³•:新增 employee_id å…³è” hrm_employee.id,姓名/工号/部门改为员工档案带出;
--       æ—§æ–‡æœ¬å­—段保留为快照,便于员工档案变更后仍可追溯开证时的人事信息。
-- è¯´æ˜Žï¼šå¹‚等,可重复执行
-- ============================================================
-- ---------- 1. æ–°å¢ž employee_id å­—段 ----------
SET @exist_col = (SELECT COUNT(*) FROM information_schema.columns
                  WHERE table_schema = DATABASE() AND table_name = 'safety_certificate' AND column_name = 'employee_id');
SET @sql = IF(@exist_col = 0,
              'ALTER TABLE `safety_certificate` ADD COLUMN `employee_id` bigint NULL COMMENT ''持证人(员工编号,关联 hrm_employee.id)'' AFTER `id`',
              'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- ---------- 2. ç´¢å¼• ----------
SET @exist_idx = (SELECT COUNT(*) FROM information_schema.statistics
                  WHERE table_schema = DATABASE() AND table_name = 'safety_certificate' AND index_name = 'idx_employee_id');
SET @sql2 = IF(@exist_idx = 0,
               'CREATE INDEX `idx_employee_id` ON `safety_certificate` (`employee_id`)',
               'SELECT 1');
PREPARE stmt2 FROM @sql2;
EXECUTE stmt2;
DEALLOCATE PREPARE stmt2;
-- ---------- 3. åŽŸä¸‰åˆ—æ”¹ä¸ºå¿«ç…§è¯­ä¹‰ï¼ˆä¿ç•™ï¼Œä¸å†ä½œä¸ºå”¯ä¸€æ¥æºï¼‰ ----------
ALTER TABLE `safety_certificate`
  MODIFY COLUMN `employee_name` varchar(50) NULL COMMENT '持证人姓名(快照,由员工档案带出)',
  MODIFY COLUMN `employee_no` varchar(50) NULL COMMENT '工号(快照,由员工档案带出)',
  MODIFY COLUMN `dept_name` varchar(100) NULL COMMENT '所属部门(快照,由员工档案带出)';
-- ---------- 4. æ ¡éªŒ ----------
SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_COMMENT
  FROM information_schema.columns
 WHERE table_schema = DATABASE() AND table_name = 'safety_certificate'
   AND COLUMN_NAME IN ('employee_id', 'employee_name', 'employee_no', 'dept_name')
 ORDER BY ORDINAL_POSITION;
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/api/employee/HrmEmployeeApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.hrm.api.employee;
import cn.iocoder.yudao.module.hrm.api.employee.dto.HrmEmployeeSimpleDTO;
import java.util.Collection;
import java.util.List;
/**
 * å‘˜å·¥æ¡£æ¡ˆ API æŽ¥å£
 *
 * <p>提供给其他模块跨模块查询员工精简信息,
 * ä¾‹å¦‚安全生产模块的特种作业证需要关联持证人。</p>
 *
 * @author è¶…级管理员
 */
public interface HrmEmployeeApi {
    /**
     * èŽ·å–å‘˜å·¥ç²¾ç®€ä¿¡æ¯
     *
     * @param id å‘˜å·¥ç¼–号
     * @return å‘˜å·¥ä¿¡æ¯ï¼Œä¸å­˜åœ¨è¿”回 null
     */
    HrmEmployeeSimpleDTO getEmployee(Long id);
    /**
     * æ‰¹é‡èŽ·å–å‘˜å·¥ç²¾ç®€ä¿¡æ¯
     *
     * @param ids å‘˜å·¥ç¼–号集合
     * @return å‘˜å·¥ä¿¡æ¯åˆ—表
     */
    List<HrmEmployeeSimpleDTO> getEmployeeList(Collection<Long> ids);
    /**
     * æ ¡éªŒå‘˜å·¥æ˜¯å¦å­˜åœ¨ï¼ˆä¸”未删除)
     *
     * @param id å‘˜å·¥ç¼–号
     * @return æ˜¯å¦å­˜åœ¨
     */
    boolean validateEmployeeExists(Long id);
}
yudao-module-hrm-api/src/main/java/cn/iocoder/yudao/module/hrm/api/employee/dto/HrmEmployeeSimpleDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,57 @@
package cn.iocoder.yudao.module.hrm.api.employee.dto;
import lombok.Data;
/**
 * å‘˜å·¥ç²¾ç®€ä¿¡æ¯ DTO
 *
 * <p>供其他模块(如安全生产模块的特种作业证)关联员工档案时使用。</p>
 *
 * @author è¶…级管理员
 */
@Data
public class HrmEmployeeSimpleDTO {
    /**
     * å‘˜å·¥ç¼–号
     */
    private Long id;
    /**
     * å…³è”的系统用户编号
     *
     * å…³è” AdminUserDO#getId()
     */
    private Long userId;
    /**
     * å‘˜å·¥ç¼–号(工号)
     */
    private String employeeNo;
    /**
     * å‘˜å·¥å§“名
     */
    private String name;
    /**
     * éƒ¨é—¨ç¼–号
     */
    private Long deptId;
    /**
     * éƒ¨é—¨åç§°
     */
    private String deptName;
    /**
     * æ‰‹æœºå·ç 
     */
    private String phone;
    /**
     * å‘˜å·¥çŠ¶æ€ï¼š1-在职,2-试用,3-离职
     */
    private Integer employeeStatus;
}
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/api/employee/HrmEmployeeApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,82 @@
package cn.iocoder.yudao.module.hrm.api.employee;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.hrm.api.employee.dto.HrmEmployeeSimpleDTO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.employee.HrmEmployeeDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.employee.HrmEmployeeMapper;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
 * å‘˜å·¥æ¡£æ¡ˆ API å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
public class HrmEmployeeApiImpl implements HrmEmployeeApi {
    @Resource
    private HrmEmployeeMapper employeeMapper;
    @Resource
    private DeptApi deptApi;
    @Override
    public HrmEmployeeSimpleDTO getEmployee(Long id) {
        if (id == null) {
            return null;
        }
        HrmEmployeeDO employee = employeeMapper.selectById(id);
        if (employee == null) {
            return null;
        }
        return build(employee, getDeptName(employee.getDeptId()));
    }
    @Override
    public List<HrmEmployeeSimpleDTO> getEmployeeList(Collection<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            return List.of();
        }
        List<HrmEmployeeDO> employees = employeeMapper.selectBatchIds(ids);
        if (employees.isEmpty()) {
            return List.of();
        }
        // æ‰¹é‡æŸ¥éƒ¨é—¨ï¼Œé¿å… N+1
        List<Long> deptIds = employees.stream().map(HrmEmployeeDO::getDeptId)
                .filter(Objects::nonNull).distinct().toList();
        Map<Long, DeptRespDTO> deptMap = deptIds.isEmpty() ? Map.of() : deptApi.getDeptMap(deptIds);
        return employees.stream()
                .map(e -> {
                    DeptRespDTO dept = deptMap.get(e.getDeptId());
                    return build(e, dept == null ? null : dept.getName());
                })
                .toList();
    }
    @Override
    public boolean validateEmployeeExists(Long id) {
        return id != null && employeeMapper.selectById(id) != null;
    }
    private String getDeptName(Long deptId) {
        if (deptId == null) {
            return null;
        }
        DeptRespDTO dept = deptApi.getDept(deptId);
        return dept == null ? null : dept.getName();
    }
    private static HrmEmployeeSimpleDTO build(HrmEmployeeDO employee, String deptName) {
        HrmEmployeeSimpleDTO dto = BeanUtils.toBean(employee, HrmEmployeeSimpleDTO.class);
        dto.setDeptName(deptName);
        return dto;
    }
}
yudao-module-mes/pom.xml
@@ -29,6 +29,12 @@
            <artifactId>yudao-module-infra</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- hrm-api:特种作业证持证人关联员工档案 -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-hrm-api</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- MES åˆ¶é€ æ‰§è¡Œ API -->
        <dependency>
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/SafetyCertificateController.java
@@ -65,7 +65,7 @@
    public CommonResult<SafetyCertificateRespVO> get(@RequestParam("id") Long id) {
        var cert = certificateService.getCertificate(id);
        var resp = BeanUtils.toBean(cert, SafetyCertificateRespVO.class);
        SafetyCertificateService.fillDisplayFields(cert, resp);
        certificateService.fillDisplayFields(cert, resp);
        return success(resp);
    }
@@ -74,9 +74,11 @@
    @PreAuthorize("@ss.hasPermission('safety:cert:query')")
    public CommonResult<PageResult<SafetyCertificateRespVO>> page(@Valid SafetyCertificatePageReqVO reqVO) {
        var page = certificateService.getCertificatePage(reqVO);
        // æ‰¹é‡è¡¥å…¨æŒè¯äººæœ€æ–°æ¡£æ¡ˆï¼ˆå§“名/工号/部门),避免逐条查询
        certificateService.fillEmployeeInfo(page.getList());
        var result = BeanUtils.toBean(page, SafetyCertificateRespVO.class);
        for (int i = 0; i < page.getList().size(); i++) {
            SafetyCertificateService.fillDisplayFields(page.getList().get(i), result.getList().get(i));
            certificateService.fillDisplayFields(page.getList().get(i), result.getList().get(i));
        }
        return success(result);
    }
@@ -87,10 +89,13 @@
    @PreAuthorize("@ss.hasPermission('safety:cert:query')")
    public CommonResult<List<SafetyCertificateRespVO>> expiringList(
            @RequestParam(value = "days", required = false) Integer days) {
        List<SafetyCertificateRespVO> list = certificateService.getExpiringCertificates(days).stream()
        List<cn.iocoder.yudao.module.mes.dal.dataobject.safety.SafetyCertificateDO> certs =
                certificateService.getExpiringCertificates(days);
        certificateService.fillEmployeeInfo(certs);
        List<SafetyCertificateRespVO> list = certs.stream()
                .map(cert -> {
                    var resp = BeanUtils.toBean(cert, SafetyCertificateRespVO.class);
                    SafetyCertificateService.fillDisplayFields(cert, resp);
                    certificateService.fillDisplayFields(cert, resp);
                    return resp;
                }).toList();
        return success(list);
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/cert/SafetyCertificatePageReqVO.java
@@ -18,7 +18,10 @@
@EqualsAndHashCode(callSuper = true)
public class SafetyCertificatePageReqVO extends PageParam {
    @Schema(description = "持证人姓名")
    @Schema(description = "持证人(员工编号)")
    private Long employeeId;
    @Schema(description = "持证人姓名(模糊匹配)")
    private String employeeName;
    @Schema(description = "工号")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/cert/SafetyCertificateRespVO.java
@@ -16,9 +16,14 @@
public class SafetyCertificateRespVO {
    private Long id;
    @Schema(description = "持证人(员工编号)")
    private Long employeeId;
    @Schema(description = "持证人关联的系统用户编号")
    private Long userId;
    private String employeeName;
    private String employeeNo;
    private String deptName;
    private String phone;
    private String certType;
    @Schema(description = "证件类型名称")
    private String certTypeName;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/safety/vo/cert/SafetyCertificateSaveReqVO.java
@@ -10,6 +10,9 @@
/**
 * å®‰å…¨ç”Ÿäº§ - ç‰¹ç§ä½œä¸šè¯æ–°å¢ž/修改 Request VO
 *
 * <p>持证人通过 employeeId å…³è”员工档案,姓名与部门由后端从档案带出,
 * å‰ç«¯åªéœ€é€‰äººï¼Œä¸æ‰‹å·¥å¡«å†™ï¼Œé¿å…ä¸ŽäººåŠ›èµ„æºæ•°æ®ä¸ä¸€è‡´ã€‚</p>
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - ç‰¹ç§ä½œä¸šè¯æ–°å¢ž/修改 Request VO")
@@ -18,18 +21,9 @@
    private Long id;
    @Schema(description = "持证人姓名", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
    @NotBlank(message = "持证人姓名不能为空")
    @Size(max = 50)
    private String employeeName;
    @Schema(description = "工号", example = "EMP001")
    @Size(max = 50)
    private String employeeNo;
    @Schema(description = "所属部门", example = "电气车间")
    @Size(max = 100)
    private String deptName;
    @Schema(description = "持证人(员工编号)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "请选择持证人")
    private Long employeeId;
    @Schema(description = "证件类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "HIGH_VOLTAGE_OPERATION")
    @NotBlank(message = "证件类型不能为空")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/safety/SafetyCertificateDO.java
@@ -22,11 +22,17 @@
    @TableId
    private Long id;
    /** æŒè¯äººå§“名 */
    /**
     * æŒè¯äººï¼ˆå‘˜å·¥ç¼–号)
     *
     * å…³è” HrmEmployeeDO#getId(),姓名/工号/部门均由此关联带出
     */
    private Long employeeId;
    /** æŒè¯äººå§“名(快照,由员工档案带出,用于员工档案变更后追溯开证时信息) */
    private String employeeName;
    /** å·¥å· */
    /** å·¥å·ï¼ˆå¿«ç…§ï¼‰ */
    private String employeeNo;
    /** æ‰€å±žéƒ¨é—¨ */
    /** æ‰€å±žéƒ¨é—¨ï¼ˆå¿«ç…§ï¼‰ */
    private String deptName;
    /** è¯ä»¶ç±»åž‹ï¼Œè§ SafetyCertificateTypeEnum */
    private String certType;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/SafetyErrorCodeConstants.java
@@ -22,6 +22,7 @@
    ErrorCode SAFETY_CERT_NOT_EXISTS = new ErrorCode(1_040_962_000, "特种作业证不存在");
    ErrorCode SAFETY_CERT_NO_DUPLICATE = new ErrorCode(1_040_962_001, "证件编号已存在");
    ErrorCode SAFETY_CERT_EXPIRE_BEFORE_ISSUE = new ErrorCode(1_040_962_002, "有效期不能早于发证日期");
    ErrorCode SAFETY_CERT_EMPLOYEE_NOT_EXISTS = new ErrorCode(1_040_962_003, "持证人不存在,请从员工档案中选择");
    // ========== å·¡æ£€ç‚¹ä½ / å·¡æ£€è®°å½• 1_040_963_xxx ==========
    ErrorCode SAFETY_POINT_NOT_EXISTS = new ErrorCode(1_040_963_000, "巡检点位不存在");
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyCertificateService.java
@@ -1,8 +1,11 @@
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;
@@ -17,7 +20,9 @@
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
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.*;
@@ -27,6 +32,10 @@
 *
 * <p>核心能力:证书到期自动预警。状态由 {@link #resolveStatus} æ ¹æ®æœ‰æ•ˆæœŸå®žæ—¶æŽ¨å¯¼ï¼Œ
 * ä¸ä¾èµ–手工维护,避免过期未及时更新状态。</p>
 *
 * <p>持证人通过 employeeId å…³è”人力资源的员工档案:
 * æ–°å¢ž/修改时从档案带出姓名、工号、部门并落为快照;查询时实时回填最新档案信息,
 * å‘˜å·¥è°ƒå²—后证件列表也会跟着显示新部门。</p>
 *
 * @author è¶…级管理员
 */
@@ -39,11 +48,13 @@
    @Resource
    private SafetyCertificateMapper certificateMapper;
    @Resource
    private HrmEmployeeApi hrmEmployeeApi;
    public Long createCertificate(SafetyCertificateSaveReqVO reqVO) {
        reqVO.setId(null);
        validateCertificateForCreate(reqVO);
        SafetyCertificateDO cert = buildCertificate(reqVO);
        SafetyCertificateDO cert = buildCertificate(reqVO, null);
        certificateMapper.insert(cert);
        return cert.getId();
    }
@@ -51,7 +62,7 @@
    public void updateCertificate(SafetyCertificateSaveReqVO reqVO) {
        SafetyCertificateDO old = validateCertificateExists(reqVO.getId());
        validateCertificateForUpdate(reqVO, old);
        SafetyCertificateDO cert = buildCertificate(reqVO);
        SafetyCertificateDO cert = buildCertificate(reqVO, old);
        certificateMapper.updateById(cert);
    }
@@ -79,15 +90,47 @@
    }
    /**
     * åˆ†é¡µç»“果的展示字段补全(类型名称、状态名称、剩余天数)
     * æ‰¹é‡è¡¥å…¨æŒè¯äººçš„æœ€æ–°å‘˜å·¥æ¡£æ¡ˆä¿¡æ¯ï¼ˆå§“名、工号、部门、手机、关联用户)
     *
     * <p>查询时用档案实时值覆盖快照值,保证员工调岗后台账同步;
     * å‘˜å·¥æ¡£æ¡ˆå·²è¢«åˆ é™¤æ—¶å›žé€€åˆ°å¿«ç…§å€¼ï¼Œä¿è¯åŽ†å²è¯ä»¶ä»å¯å±•ç¤ºã€‚</p>
     */
    public static void fillDisplayFields(SafetyCertificateDO cert, SafetyCertificateRespVO respVO) {
    public void fillEmployeeInfo(List<SafetyCertificateDO> certs) {
        List<Long> employeeIds = certs.stream()
                .map(SafetyCertificateDO::getEmployeeId)
                .filter(Objects::nonNull).distinct().toList();
        Map<Long, HrmEmployeeSimpleDTO> employeeMap = employeeIds.isEmpty() ? Map.of()
                : 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 fillDisplayFields(SafetyCertificateDO cert, SafetyCertificateRespVO respVO) {
        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()));
        }
        HrmEmployeeSimpleDTO employee = cert.getEmployeeId() == null ? null
                : hrmEmployeeApi.getEmployee(cert.getEmployeeId());
        if (employee != null) {
            respVO.setEmployeeName(employee.getName());
            respVO.setEmployeeNo(employee.getEmployeeNo());
            respVO.setDeptName(employee.getDeptName());
            respVO.setPhone(employee.getPhone());
            respVO.setUserId(employee.getUserId());
        }
    }
@@ -116,12 +159,24 @@
        return SafetyCertificateStatusEnum.VALID.getStatus();
    }
    private SafetyCertificateDO buildCertificate(SafetyCertificateSaveReqVO reqVO) {
    /**
     * æž„建 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.setEmployeeName(StrUtil.trim(reqVO.getEmployeeName()));
        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());
        cert.setStatus(resolveStatus(reqVO.getExpireDate(), cert.getWarnDays(), reqVO.getStatus()));
        // å·²æ³¨é”€çŠ¶æ€ä¸å…è®¸é€šè¿‡ç¼–è¾‘æ”¹å›ž
        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;
    }
@@ -138,11 +193,6 @@
        SafetyCertificateDO byCertNo = certificateMapper.selectByCertNo(StrUtil.trim(reqVO.getCertNo()));
        if (byCertNo != null && !byCertNo.getId().equals(old.getId())) {
            throw exception(SAFETY_CERT_NO_DUPLICATE);
        }
        // ä¿ç•™åŽŸçŠ¶æ€ä¸­çš„ã€Œå·²æ³¨é”€ã€ï¼Œä¸å…è®¸é€šè¿‡ç¼–è¾‘éšæ„æ”¹å›ž
        if (SafetyCertificateStatusEnum.REVOKED.getStatus().equals(old.getStatus())
                && !SafetyCertificateStatusEnum.REVOKED.getStatus().equals(reqVO.getStatus())) {
            reqVO.setStatus(SafetyCertificateStatusEnum.REVOKED.getStatus());
        }
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyLedgerExportService.java
@@ -105,8 +105,10 @@
        var pageReqVO = new cn.iocoder.yudao.module.mes.controller.admin.safety.vo.cert.SafetyCertificatePageReqVO();
        pageReqVO.setPageNo(1);
        pageReqVO.setPageSize(50_000);
        List<List<Object>> rows = certificateService.getCertificatePage(pageReqVO)
                .getList().stream()
        var certs = certificateService.getCertificatePage(pageReqVO).getList();
        // å¯¼å‡ºæ—¶ç”¨å‘˜å·¥æ¡£æ¡ˆçš„æœ€æ–°å§“名/工号/部门
        certificateService.fillEmployeeInfo(certs);
        List<List<Object>> rows = certs.stream()
                .map(c -> Arrays.<Object>asList(
                        str(c.getEmployeeName()), str(c.getEmployeeNo()), str(c.getDeptName()),
                        SafetyCertificateTypeEnum.getNameByType(c.getCertType()), str(c.getCertNo()),