Files
his/null_key_error_analysis.md

6.6 KiB
Raw Blame History

"element cannot be mapped to a null key" 错误分析报告

错误根本原因

该错误发生在 Java Stream 使用 Collectors.groupingByCollectors.toMap 时,key 为 null 导致的。

Java 的 HashMap 不允许 null keyHashMap 的实现有关),当试图将元素映射到 null key 时,就会抛出此异常。

问题定位

1. 数据层问题 - SQL 查询返回 null

位置1: ChargeItemMapper.xml (第71行)

文件: openhis-server-new/openhis-domain/src/main/resources/mapper/administration/ChargeItemMapper.xml

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

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

// 2333-2334行
Map<String, List<ChargeItemBaseInfoDto>> 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

// 2461-2462行
Map<String, List<ChargeItemBaseInfoDto>> 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

// 2477-2478行
Map<Long, List<PaymentRecDetail>> 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

// 935-936行
Map<String, List<PaymentRecDetailAccountResult>> 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

// 1484-1485行
Map<String, List<PaymentRecDetailAccountResult>> paymentDetailsMapByContract = PaymentRecDetailAccountResultList
    .stream().collect(Collectors.groupingBy(PaymentRecDetailAccountResult::getContractNo));

风险: 同上,如果 contractNo 为 null将抛出异常。


修复建议

方案1: 修改 SQL 查询,使用 COALESCE 处理 null (推荐)

修改 ChargeItemMapper.xml 第71行

-- 修改前
contract.bus_no as contract_no,

-- 修改后
COALESCE(contract.bus_no, 'DEFAULT') as contract_no,

修改 PaymentRecDetailMapper.xml 第37行

-- 修改前
T2.contract_no,

-- 修改后
COALESCE(T2.contract_no, 'DEFAULT') as contract_no,

方案2: 修改 Java 代码,过滤 null key

在使用 groupingBy 之前过滤掉 key 为 null 的数据:

// 修改前
Map<String, List<ChargeItemBaseInfoDto>> chargeItemKVByContractNo
    = chargeItemBaseInfoByIds.stream().collect(Collectors.groupingBy(ChargeItemBaseInfoDto::getContractNo));

// 修改后
Map<String, List<ChargeItemBaseInfoDto>> chargeItemKVByContractNo
    = chargeItemBaseInfoByIds.stream()
        .filter(dto -> dto.getContractNo() != null)
        .collect(Collectors.groupingBy(ChargeItemBaseInfoDto::getContractNo));

方案3: 使用 null-safe 的收集器

自定义一个处理 null key 的收集器:

public static <T, K> Collector<T, ?, Map<K, List<T>>> groupingByNullSafe(
        Function<? super T, ? extends K> classifier) {
    return Collectors.groupingBy(
        classifier,
        HashMap::new,
        Collectors.toList()
    );
}

建议修复优先级

  1. 高优先级: 修改 ChargeItemMapper.xmlPaymentRecDetailMapper.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 字段值
// 调试代码示例
chargeItemBaseInfoByIds.forEach(dto -> {
    System.out.println("ChargeItem ID: " + dto.getId() + ", contractNo: " + dto.getContractNo());
});