# "element cannot be mapped to a null key" 错误分析报告 ## 错误根本原因 该错误发生在 Java Stream 使用 `Collectors.groupingBy` 或 `Collectors.toMap` 时,**key 为 null** 导致的。 Java 的 `HashMap` 不允许 null key(与 `HashMap` 的实现有关),当试图将元素映射到 null key 时,就会抛出此异常。 ## 问题定位 ### 1. 数据层问题 - SQL 查询返回 null #### 位置1: ChargeItemMapper.xml (第71行) **文件**: `openhis-server-new/openhis-domain/src/main/resources/mapper/administration/ChargeItemMapper.xml` ```sql SELECT ... contract.bus_no as contract_no, -- 第71行 ... FROM adm_charge_item a ... LEFT JOIN adm_account acc ON a.account_id = acc.id LEFT JOIN fin_contract contract ON acc.contract_no = contract.bus_no -- LEFT JOIN ``` **问题**: 使用 LEFT JOIN 关联 `fin_contract` 表,当 `acc.contract_no` 为 null 或没有匹配的合同记录时,`contract.bus_no` 会返回 null。 #### 位置2: PaymentRecDetailMapper.xml (第37行) **文件**: `openhis-server-new/openhis-domain/src/main/resources/mapper/financial/PaymentRecDetailMapper.xml` ```sql SELECT ... T2.contract_no, -- 第37行 ... FROM fin_payment_rec_detail T1 LEFT JOIN adm_account T2 ON T1.account_id = T2."id" -- LEFT JOIN ``` **问题**: 使用 LEFT JOIN 关联 `adm_account` 表,当 `T1.account_id` 为 null 或没有匹配的账户记录时,`T2.contract_no` 会返回 null。 --- ### 2. 业务层问题 - 使用 groupingBy 时 key 可能为 null #### 位置1: PaymentRecServiceImpl.java (第2334行) **文件**: `openhis-server-new/openhis-application/src/main/java/com/openhis/web/paymentmanage/appservice/impl/PaymentRecServiceImpl.java` ```java // 2333-2334行 Map> chargeItemKVByContractNo = chargeItemBaseInfoByIds.stream().collect(Collectors.groupingBy(ChargeItemBaseInfoDto::getContractNo)); ``` **风险**: 如果 `ChargeItemBaseInfoDto.contractNo` 为 null(来自上述 SQL 查询),将抛出异常。 #### 位置2: PaymentRecServiceImpl.java (第2462行) **文件**: `openhis-server-new/openhis-application/src/main/java/com/openhis/web/paymentmanage/appservice/impl/PaymentRecServiceImpl.java` ```java // 2461-2462行 Map> chargeItemKVByContractNo = chargeItemBaseInfoByIds.stream().collect(Collectors.groupingBy(ChargeItemBaseInfoDto::getContractNo)); ``` **风险**: 同上,如果 `contractNo` 为 null,将抛出异常。 #### 位置3: PaymentRecServiceImpl.java (第2478行) **文件**: `openhis-server-new/openhis-application/src/main/java/com/openhis/web/paymentmanage/appservice/impl/PaymentRecServiceImpl.java` ```java // 2477-2478行 Map> payTransNoMap = paymentRecDetails.stream().collect(Collectors.groupingBy(PaymentRecDetail::getAccountId)); ``` **风险**: 如果 `PaymentRecDetail.accountId` 为 null,将抛出异常。 #### 位置4: IChargeBillServiceImpl.java (第936行) **文件**: `openhis-server-new/openhis-application/src/main/java/com/openhis/web/paymentmanage/appservice/impl/IChargeBillServiceImpl.java` ```java // 935-936行 Map> paymentDetailsMapByContract = PaymentRecDetailAccountResultList .stream().collect(Collectors.groupingBy(PaymentRecDetailAccountResult::getContractNo)); ``` **风险**: 如果 `PaymentRecDetailAccountResult.contractNo` 为 null(来自上述 SQL 查询),将抛出异常。 #### 位置5: IChargeBillServiceImpl.java (第1485行) **文件**: `openhis-server-new/openhis-application/src/main/java/com/openhis/web/paymentmanage/appservice/impl/IChargeBillServiceImpl.java` ```java // 1484-1485行 Map> paymentDetailsMapByContract = PaymentRecDetailAccountResultList .stream().collect(Collectors.groupingBy(PaymentRecDetailAccountResult::getContractNo)); ``` **风险**: 同上,如果 `contractNo` 为 null,将抛出异常。 --- ## 修复建议 ### 方案1: 修改 SQL 查询,使用 COALESCE 处理 null (推荐) 修改 `ChargeItemMapper.xml` 第71行: ```sql -- 修改前 contract.bus_no as contract_no, -- 修改后 COALESCE(contract.bus_no, 'DEFAULT') as contract_no, ``` 修改 `PaymentRecDetailMapper.xml` 第37行: ```sql -- 修改前 T2.contract_no, -- 修改后 COALESCE(T2.contract_no, 'DEFAULT') as contract_no, ``` ### 方案2: 修改 Java 代码,过滤 null key 在使用 `groupingBy` 之前过滤掉 key 为 null 的数据: ```java // 修改前 Map> chargeItemKVByContractNo = chargeItemBaseInfoByIds.stream().collect(Collectors.groupingBy(ChargeItemBaseInfoDto::getContractNo)); // 修改后 Map> chargeItemKVByContractNo = chargeItemBaseInfoByIds.stream() .filter(dto -> dto.getContractNo() != null) .collect(Collectors.groupingBy(ChargeItemBaseInfoDto::getContractNo)); ``` ### 方案3: 使用 null-safe 的收集器 自定义一个处理 null key 的收集器: ```java public static Collector>> groupingByNullSafe( Function classifier) { return Collectors.groupingBy( classifier, HashMap::new, Collectors.toList() ); } ``` --- ## 建议修复优先级 1. **高优先级**: 修改 `ChargeItemMapper.xml` 和 `PaymentRecDetailMapper.xml` 的 SQL 查询 - 这是根本原因,修复后可以防止 null 值传播到 Java 层 2. **中优先级**: 修改 `PaymentRecServiceImpl.java` 第2334行和第2462行 - 这是门诊收费和住院结算的关键路径 3. **低优先级**: 修改 `IChargeBillServiceImpl.java` 第936行和第1485行 - 这是收费账单相关功能 --- ## 与最近修改的关系 用户提到最近修改了 `OutpatientChargeAppMapper.xml`,增加了 `cli_surgery` 表关联。 虽然这个修改本身不会直接导致 `contract_no` 为 null,但可能触发了某些收费流程,使得原本不会执行的代码路径被执行,从而暴露了这个潜在问题。 根本原因还是 `ChargeItemMapper.xml` 中的 SQL 查询使用了 LEFT JOIN 导致 `contract_no` 可能为 null。 --- ## 验证方法 1. 检查数据库中是否存在 `account_id` 为 null 的 `adm_charge_item` 记录 2. 检查数据库中是否存在 `contract_no` 为 null 的 `adm_account` 记录 3. 在出现错误的收费操作中,打印相关 DTO 对象的 `contractNo` 字段值 ```java // 调试代码示例 chargeItemBaseInfoByIds.forEach(dto -> { System.out.println("ChargeItem ID: " + dto.getId() + ", contractNo: " + dto.getContractNo()); }); ```