6.6 KiB
"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
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()
);
}
建议修复优先级
-
高优先级: 修改
ChargeItemMapper.xml和PaymentRecDetailMapper.xml的 SQL 查询- 这是根本原因,修复后可以防止 null 值传播到 Java 层
-
中优先级: 修改
PaymentRecServiceImpl.java第2334行和第2462行- 这是门诊收费和住院结算的关键路径
-
低优先级: 修改
IChargeBillServiceImpl.java第936行和第1485行- 这是收费账单相关功能
与最近修改的关系
用户提到最近修改了 OutpatientChargeAppMapper.xml,增加了 cli_surgery 表关联。
虽然这个修改本身不会直接导致 contract_no 为 null,但可能触发了某些收费流程,使得原本不会执行的代码路径被执行,从而暴露了这个潜在问题。
根本原因还是 ChargeItemMapper.xml 中的 SQL 查询使用了 LEFT JOIN 导致 contract_no 可能为 null。
验证方法
- 检查数据库中是否存在
account_id为 null 的adm_charge_item记录 - 检查数据库中是否存在
contract_no为 null 的adm_account记录 - 在出现错误的收费操作中,打印相关 DTO 对象的
contractNo字段值
// 调试代码示例
chargeItemBaseInfoByIds.forEach(dto -> {
System.out.println("ChargeItem ID: " + dto.getId() + ", contractNo: " + dto.getContractNo());
});