Merge develop into test - sync latest code
This commit is contained in:
193
null_key_error_analysis.md
Normal file
193
null_key_error_analysis.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# "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());
|
||||
});
|
||||
```
|
||||
Reference in New Issue
Block a user