Files
his/null_key_error_analysis.md

194 lines
6.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# "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<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`
```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`
```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`
```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`
```java
// 1484-1485行
Map<String, List<PaymentRecDetailAccountResult>> 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<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 的收集器:
```java
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.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());
});
```