xiaoyi
9 天以前 2b161b1f1401c89fa927e390d78ff49568337f6c
fix: 修复 Map.of() 空集合导致的 NPE(组织/证件等未关联员工时接口 500)

根因:Map.of() 是不可变空 Map,它的 get(null) 会抛 NPE(Objects.requireNonNull),而 put 会抛 UnsupportedOperationException。业务上 leaderId / employeeId / deptId 等外键允许为空,空集合场景下 map.get(null) 必然命中,导致列表接口 500。

修复范围(Map.of() -> new HashMap<>()):

1. SafetyRefResolver.employeeMap / workshopMap (4 处,安全生产模块组织树、证件、隐患、检查、培训、作业票等全部列表)

2. SafetyCertificateService.fillEmployeeInfo

3. HrmEmployeeApiImpl.getEmployeeList(员工 deptId 为空时)

4. OperateLogController.buildResponses(预防性统一)

另附回归测试 SafetyRefResolverEmptyCollectionTest,锁死空集合必须允许 null key 查询且返回可变 Map。

注:Collections.emptyMap() 的 get(null) 是安全的,不受影响,无需改动。
已添加1个文件
已修改4个文件
134 ■■■■■ 文件已修改
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/api/employee/HrmEmployeeApiImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyCertificateService.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyRefResolver.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/safety/SafetyRefResolverEmptyCollectionTest.java 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/OperateLogController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/api/employee/HrmEmployeeApiImpl.java
@@ -10,6 +10,7 @@
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -51,7 +52,9 @@
        // æ‰¹é‡æŸ¥éƒ¨é—¨ï¼Œé¿å… 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);
        // ç”¨ new HashMap<>() è€Œéž Map.of():员工的 deptId å…è®¸ä¸ºç©ºï¼Œ
        // åŽé¢ deptMap.get(e.getDeptId()) ä¼š get(null),Map.of() çš„ get(null) æŠ› NPE。
        Map<Long, DeptRespDTO> deptMap = deptIds.isEmpty() ? new HashMap<>() : deptApi.getDeptMap(deptIds);
        return employees.stream()
                .map(e -> {
                    DeptRespDTO dept = deptMap.get(e.getDeptId());
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyCertificateService.java
@@ -19,6 +19,7 @@
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;
@@ -101,7 +102,10 @@
        List<Long> employeeIds = certs.stream()
                .map(SafetyCertificateDO::getEmployeeId)
                .filter(Objects::nonNull).distinct().toList();
        Map<Long, HrmEmployeeSimpleDTO> employeeMap = employeeIds.isEmpty() ? Map.of()
        // ç”¨ new HashMap<>() è€Œéž Map.of():employeeId å…è®¸ä¸º null,
        // åŽé¢ employeeMap.get(cert.getEmployeeId()) åœ¨ç©ºé›†åˆåœºæ™¯ä¼š get(null),
        // Map.of() çš„ get(null) ä¼šæŠ› NPE å¯¼è‡´è¯ä»¶åˆ—表接口 500。
        Map<Long, HrmEmployeeSimpleDTO> employeeMap = employeeIds.isEmpty() ? new HashMap<>()
                : hrmEmployeeApi.getEmployeeList(employeeIds).stream()
                        .collect(Collectors.toMap(HrmEmployeeSimpleDTO::getId, e -> e, (a, b) -> a));
        certs.forEach(cert -> {
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/safety/SafetyRefResolver.java
@@ -9,6 +9,7 @@
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -65,12 +66,15 @@
     * æ‰¹é‡å–员工,返回 id -> å‘˜å·¥ çš„ Map
     */
    public Map<Long, HrmEmployeeSimpleDTO> employeeMap(Collection<Long> employeeIds) {
        // æ³¨æ„ï¼šè¿™é‡Œå¿…须返回可接受 null key çš„空 Map。
        // è°ƒç”¨æ–¹ä¼šç›´æŽ¥ map.get(记录里的 xxxId),而 xxxId å…è®¸ä¸º null(例如组织未设负责人、
        // è¯ä»¶æœªå…³è”持证人),Map.of() çš„ get(null) ä¼šæŠ› NPE,导致列表接口 500。
        if (CollUtil.isEmpty(employeeIds)) {
            return Map.of();
            return new HashMap<>();
        }
        List<Long> ids = employeeIds.stream().filter(Objects::nonNull).distinct().toList();
        if (ids.isEmpty()) {
            return Map.of();
            return new HashMap<>();
        }
        return hrmEmployeeApi.getEmployeeList(ids).stream()
                .collect(Collectors.toMap(HrmEmployeeSimpleDTO::getId, e -> e, (a, b) -> a));
@@ -103,12 +107,13 @@
     * æ‰¹é‡å–车间,返回 id -> è½¦é—´ çš„ Map
     */
    public Map<Long, MesMdWorkshopDO> workshopMap(Collection<Long> workshopIds) {
        // åŒä¸Šï¼šè½¦é—´ç¼–号允许为 null,空集合必须返回可接受 null key çš„ Map
        if (CollUtil.isEmpty(workshopIds)) {
            return Map.of();
            return new HashMap<>();
        }
        List<Long> ids = workshopIds.stream().filter(Objects::nonNull).distinct().toList();
        if (ids.isEmpty()) {
            return Map.of();
            return new HashMap<>();
        }
        return workshopMapper.selectBatchIds(ids).stream()
                .collect(Collectors.toMap(MesMdWorkshopDO::getId, w -> w, (a, b) -> a));
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/safety/SafetyRefResolverEmptyCollectionTest.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,105 @@
package cn.iocoder.yudao.module.mes.service.safety;
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.dal.dataobject.md.workstation.MesMdWorkshopDO;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
 * {@link SafetyRefResolver} ç©ºé›†åˆåœºæ™¯å›žå½’测试
 *
 * <p>背景:{@code Map.of()} æ˜¯ç©ºä¸”不可变的 Map,它的 {@code get(null)} ä¼šæŠ› NPE,
 * è€Œ {@code Map.of().get(非null)} ä¸ä¼šã€‚安全生产模块里「组织 / è¯ä»¶ / é𐿂£ã€ç­‰
 * è®°å½•在未关联员工(leaderId、employeeId ä¸º null)时,就会走到 {@code map.get(null)},
 * ä»Žè€Œåœ¨åˆ—表页直接 500。</p>
 *
 * <p>这里锁死约定:空集合必须返回可接受 null key çš„空 Map。</p>
 */
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class SafetyRefResolverEmptyCollectionTest {
    @InjectMocks
    private SafetyRefResolver resolver;
    @Mock
    private HrmEmployeeApi hrmEmployeeApi;
    @Mock
    private cn.iocoder.yudao.module.mes.dal.mysql.md.workstation.MesMdWorkshopMapper workshopMapper;
    /** ç©ºé›†åˆï¼šå¿…须能 get(null) è€Œä¸æŠ› NPE(这就是线上 NPE çš„æ ¹å› ï¼‰ */
    @Test
    void employeeMap_emptyCollection_getNullShouldNotThrow() {
        Map<Long, HrmEmployeeSimpleDTO> map = resolver.employeeMap(List.of());
        assertTrue(map.isEmpty());
        // Map.of() åœ¨è¿™é‡Œä¼šæŠ› NullPointerException
        assertNull(map.get(null), "空集合返回的 Map å¿…须允许用 null ä½œä¸º key æŸ¥è¯¢");
    }
    /** é›†åˆé‡Œå…¨æ˜¯ null:过滤后为空,同样必须能 get(null) */
    @Test
    void employeeMap_allNullElements_getNullShouldNotThrow() {
        Map<Long, HrmEmployeeSimpleDTO> map = resolver.employeeMap(java.util.Arrays.asList((Long) null, null));
        assertTrue(map.isEmpty());
        assertNull(map.get(null));
    }
    /** null é›†åˆæœ¬èº« */
    @Test
    void employeeMap_nullCollection_getNullShouldNotThrow() {
        Map<Long, HrmEmployeeSimpleDTO> map = resolver.employeeMap(null);
        assertTrue(map.isEmpty());
        assertNull(map.get(null));
    }
    @Test
    void workshopMap_emptyCollection_getNullShouldNotThrow() {
        Map<Long, MesMdWorkshopDO> map = resolver.workshopMap(List.of());
        assertTrue(map.isEmpty());
        assertNull(map.get(null), "空集合返回的 Map å¿…须允许用 null ä½œä¸º key æŸ¥è¯¢");
    }
    @Test
    void workshopMap_allNullElements_getNullShouldNotThrow() {
        Map<Long, MesMdWorkshopDO> map = resolver.workshopMap(java.util.Arrays.asList((Long) null));
        assertTrue(map.isEmpty());
        assertNull(map.get(null));
    }
    @Test
    void workshopMap_nullCollection_getNullShouldNotThrow() {
        Map<Long, MesMdWorkshopDO> map = resolver.workshopMap(null);
        assertTrue(map.isEmpty());
        assertNull(map.get(null));
    }
    /** è¿”回的 Map åº”该是可变的(避免后续 put æŠ› UnsupportedOperationException) */
    @Test
    void employeeMap_emptyResultShouldBeMutable() {
        Map<Long, HrmEmployeeSimpleDTO> map = resolver.employeeMap(List.of());
        map.put(1L, new HrmEmployeeSimpleDTO());
        assertEquals(1, map.size());
    }
    @Test
    void workshopMap_emptyResultShouldBeMutable() {
        Map<Long, MesMdWorkshopDO> map = resolver.workshopMap(List.of());
        map.put(1L, new MesMdWorkshopDO());
        assertEquals(1, map.size());
    }
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/OperateLogController.java
@@ -28,6 +28,7 @@
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -86,7 +87,9 @@
        Set<Long> userIds = responses.stream()
                .filter(log -> Objects.equals(log.getUserType(), UserTypeEnum.ADMIN.getValue()))
                .map(OperateLogRespVO::getUserId).filter(Objects::nonNull).collect(Collectors.toSet());
        Map<Long, AdminUserDO> users = userIds.isEmpty() ? Map.of() : adminUserService.getUserMap(userIds);
        // ç”¨ new HashMap<>() è€Œéž Map.of():下面会按 log.getUserId() å–值,
        // éžç®¡ç†å‘˜åˆ†æ”¯è™½ç„¶å·²åˆ¤ç©ºï¼Œä½†ä¿æŒä¸€è‡´çš„写法可避免以后改动踩到 Map.of() çš„ get(null) NPE。
        Map<Long, AdminUserDO> users = userIds.isEmpty() ? new HashMap<>() : adminUserService.getUserMap(userIds);
        for (OperateLogRespVO log : responses) {
            if (log.getUserId() == null) {
                log.setUserName("未知操作人");