Compare commits
6 Commits
HEAD
...
8e3bd5aeb3
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e3bd5aeb3 | |||
| 090c99d409 | |||
| f3855c9d30 | |||
| 1136a479d1 | |||
| f519d83ed1 | |||
|
|
cfbd375a48 |
36
BUG_401_ANALYSIS.md
Normal file
36
BUG_401_ANALYSIS.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Bug #401 分析报告
|
||||
|
||||
## 问题描述
|
||||
门诊完诊审计日志错误:div_log 表中 pool_id 与 slot_id 存值与设计规范不符。
|
||||
|
||||
## 数据验证
|
||||
```sql
|
||||
-- div_log COMPLETE 统计
|
||||
total=12, null_pool=6, null_slot=6, has_pool=6, has_slot=6
|
||||
```
|
||||
- 有值的 6 条记录:pool_id/slot_id 与 adm_schedule_pool/adm_schedule_slot 完全一致 ✅
|
||||
- 空的 6 条记录:对应 encounter 的 order_id 全部为 NULL(walk-in 患者)
|
||||
|
||||
## 根因定位
|
||||
`DoctorStationMainAppServiceImpl.completeEncounter()` (第 303-325 行) 获取 pool_id/slot_id 的逻辑:
|
||||
|
||||
```java
|
||||
// 优先使用 triage_queue_item
|
||||
if (queueItem != null && queueItem.getPoolId() != null && queueItem.getSlotId() != null) {
|
||||
divPoolId = queueItem.getPoolId();
|
||||
divSlotId = queueItem.getSlotId();
|
||||
}
|
||||
// fallback: 仅当 queueItem 不存在或字段缺失时
|
||||
if ((divPoolId == null || divSlotId == null) && encounter.getOrderId() != null) {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**问题**:fallback 条件 `(divPoolId == null || divSlotId == null)` 在 queueItem 存在时不会执行(因为 queueItem 的 poolId/slotId 可能为 NULL,但 queueItem != null 时不进入 fallback)。实际上,对于有 encounter.orderId 的患者(挂号患者),应该始终通过 order → schedule_slot 获取权威的 pool_id/slot_id。
|
||||
|
||||
## 修复方案
|
||||
调整 fallback 逻辑:只要有 encounter.orderId,就通过 order → schedule_slot 获取 pool_id/slot_id,不再依赖 queueItem 的字段值。queueItem 仅用于确定是否需要写审计日志的时机判断。
|
||||
|
||||
## 影响范围
|
||||
- 修改文件:`DoctorStationMainAppServiceImpl.java`(约 10 行调整)
|
||||
- 不涉及数据库 DDL 变更
|
||||
@@ -300,16 +300,12 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 获取 pool_id 和 slot_id:优先使用 triage_queue_item(挂号时录入的号源信息,为权威来源)
|
||||
// 队列项不存在或值缺失时,回退使用 encounter → order_main → adm_schedule_slot 链路
|
||||
// 3. 获取 pool_id 和 slot_id:优先使用 encounter.orderId → order_main → adm_schedule_slot 链路
|
||||
// (order_main.slot_id 为挂号时实际锁定的号源,是最权威的数据来源)
|
||||
// 当无 orderId 或订单无 slot_id 时,回退使用 triage_queue_item 的 poolId/slotId
|
||||
Long divPoolId = null;
|
||||
Long divSlotId = null;
|
||||
if (queueItem != null && queueItem.getPoolId() != null && queueItem.getSlotId() != null) {
|
||||
divPoolId = queueItem.getPoolId();
|
||||
divSlotId = queueItem.getSlotId();
|
||||
}
|
||||
// 队列项 poolId/slotId 缺失时,通过 encounter.orderId → order_main.slot_id → adm_schedule_slot.pool_id 回退获取
|
||||
if ((divPoolId == null || divSlotId == null) && encounter.getOrderId() != null) {
|
||||
if (encounter.getOrderId() != null) {
|
||||
try {
|
||||
Order order = iOrderService.getById(encounter.getOrderId());
|
||||
if (order != null && order.getSlotId() != null) {
|
||||
@@ -320,7 +316,16 @@ public class DoctorStationMainAppServiceImpl implements IDoctorStationMainAppSer
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("回退获取完诊div_log的pool_id/slot_id失败,encounterId={}", encounterId, e);
|
||||
log.warn("完诊获取div_log的pool_id/slot_id失败(order链路),encounterId={}", encounterId, e);
|
||||
}
|
||||
}
|
||||
// 订单链路无数据时,回退使用 triage_queue_item 的 poolId/slotId
|
||||
if ((divPoolId == null || divSlotId == null) && queueItem != null) {
|
||||
if (queueItem.getPoolId() != null) {
|
||||
divPoolId = queueItem.getPoolId();
|
||||
}
|
||||
if (queueItem.getSlotId() != null) {
|
||||
divSlotId = queueItem.getSlotId();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -582,15 +582,30 @@ function handleResetSearch() {
|
||||
// 初始化默认日期范围为近一周
|
||||
handleResetSearch();
|
||||
|
||||
// 🔧 BugFix#426: 懒加载套餐明细
|
||||
// 🔧 BugFix#426/#430: 懒加载套餐明细(支持 packageName 解析)
|
||||
async function loadPackageDetails(row, treeNode, resolve) {
|
||||
if (!row.packageId) {
|
||||
let packageId = row.packageId;
|
||||
if (!packageId && row.packageName) {
|
||||
try {
|
||||
const pkgRes = await listCheckPackage({ packageName: row.packageName });
|
||||
let packages = pkgRes?.data || [];
|
||||
if (!Array.isArray(packages)) {
|
||||
packages = packages.records || packages.data || [];
|
||||
}
|
||||
if (packages.length > 0) {
|
||||
packageId = packages[0].id;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('套餐名称解析失败:', err);
|
||||
}
|
||||
}
|
||||
if (!packageId) {
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await request({
|
||||
url: `/system/check-type/package/${row.packageId}/details`,
|
||||
url: `/system/check-type/package/${packageId}/details`,
|
||||
method: 'get'
|
||||
});
|
||||
const list = parsePackageDetailsPayload(res);
|
||||
@@ -624,9 +639,9 @@ function getPackageDetailsList(item) {
|
||||
return Array.isArray(carrier?.packageDetails) ? carrier.packageDetails : [];
|
||||
}
|
||||
|
||||
/** 有套餐 ID 的已选行才展示右侧套餐区(加载中 / 空 / 明细列表) */
|
||||
/** 有套餐 ID 或 packageName 的已选行才展示右侧套餐区(加载中 / 空 / 明细列表) */
|
||||
function shouldShowPackageBody(item) {
|
||||
return !!getPackageCarrier(item)?.packageId;
|
||||
return !!(getPackageCarrier(item)?.packageId || item.packageName || item.packageId);
|
||||
}
|
||||
|
||||
/** 金额展示:统一两位小数 */
|
||||
@@ -1058,7 +1073,8 @@ const filteredCategoryList = computed(() => {
|
||||
const key = dictSearchKey.value.toLowerCase();
|
||||
return categoryList.value.map(cat => ({
|
||||
...cat,
|
||||
items: cat.items.filter(item => (item.name || '').toLowerCase().includes(key))
|
||||
items: cat.items.filter(item => (item.name || '').toLowerCase().includes(key)),
|
||||
methods: cat.methods || []
|
||||
})).filter(cat => cat.items.length > 0);
|
||||
});
|
||||
|
||||
@@ -1365,10 +1381,10 @@ async function handleMethodSelect(checked, method, cat) {
|
||||
const existingItem = selectedItems.value.find(s => s.id === targetItem.id);
|
||||
if (existingItem) {
|
||||
existingItem.selectedMethod = method;
|
||||
// 从方法中获取套餐信息
|
||||
if (method.packageId) {
|
||||
// 从方法中获取套餐信息(支持 packageId 或 packageName 解析)
|
||||
if (method.packageId || method.packageName) {
|
||||
existingItem.isPackage = true;
|
||||
existingItem.packageId = method.packageId;
|
||||
existingItem.packageId = method.packageId || existingItem.packageId;
|
||||
existingItem.hasChildren = true; // #426修复
|
||||
existingItem.packageName = method.packageName || existingItem.packageName; // #428修复: 确保 packageName 同步
|
||||
// 预加载套餐明细
|
||||
@@ -1405,12 +1421,12 @@ async function handleMethodSelect(checked, method, cat) {
|
||||
isPackage: !!method.packageId || !!targetItem.packageName,
|
||||
packageId: method.packageId || targetItem.packageId || null,
|
||||
packageName: method.packageName || targetItem.packageName || null, // #428修复: 复制 packageName,确保套餐明细可加载
|
||||
hasChildren: !!(method.packageId || targetItem.packageId) // #426修复: 树形表格懒加载展开标记
|
||||
hasChildren: !!(method.packageId || method.packageName || targetItem.packageId || targetItem.packageName) // #426修复: 树形表格懒加载展开标记,支持 packageName 解析
|
||||
};
|
||||
selectedItems.value.push(newItem);
|
||||
|
||||
// 如果是套餐,预加载套餐明细
|
||||
if (newItem.isPackage && newItem.packageId) {
|
||||
if (newItem.isPackage && (newItem.packageId || newItem.packageName)) {
|
||||
loadPackageDetailsForItem(newItem);
|
||||
}
|
||||
|
||||
@@ -1548,7 +1564,7 @@ async function toggleItemExpand(item) {
|
||||
async function selectMethodCheckbox(checked, item, method) {
|
||||
if (checked) {
|
||||
item.selectedMethod = method;
|
||||
if (item.expanded && method.packageId) {
|
||||
if (item.expanded && (method.packageId || method.packageName)) {
|
||||
loadPackageDetailsForItem(item);
|
||||
}
|
||||
// 动态加载该方法对应的套餐明细
|
||||
@@ -1613,8 +1629,9 @@ async function loadMethodPackageDetails(item, method) {
|
||||
/** 检查明细表格中切换检查方法 */
|
||||
async function onDetailMethodChange(row, val) {
|
||||
row.selectedMethod = val || null;
|
||||
if (val?.packageId) {
|
||||
row.packageId = val.packageId;
|
||||
if (val?.packageId || val?.packageName) {
|
||||
row.packageId = val.packageId || row.packageId;
|
||||
row.packageName = val.packageName || row.packageName;
|
||||
row.isPackage = true;
|
||||
row.hasChildren = true; // #426修复
|
||||
}
|
||||
|
||||
@@ -1581,10 +1581,10 @@ function handleSaveGroup(orderGroupList) {
|
||||
therapyEnum: item.orderDetailInfos?.therapyEnum || '1',
|
||||
};
|
||||
|
||||
// 预初始化空行
|
||||
// 预初始化空行(组套项带预填值,设为 false 让明细字段在表格中直接展示)
|
||||
prescriptionList.value[rowIndex.value] = {
|
||||
uniqueKey: nextId.value++,
|
||||
isEdit: true,
|
||||
isEdit: false,
|
||||
statusEnum: 1,
|
||||
};
|
||||
|
||||
|
||||
@@ -1131,8 +1131,7 @@ function handleLocationClick(item, row, index) {
|
||||
.then((res) => {
|
||||
const list = res.data || [];
|
||||
const d = pickBestOrgQuantityRow(list);
|
||||
const strictOk = d && Number(d.orgQuantity ?? 0) > 0;
|
||||
if (strictOk) {
|
||||
if (d) {
|
||||
applyFromDto(d, false);
|
||||
if (Number(r.totalQuantity) <= 0) {
|
||||
proxy.$message.warning('仓库数量为0,无法调用!');
|
||||
@@ -1144,11 +1143,15 @@ function handleLocationClick(item, row, index) {
|
||||
return runGet(false).then((res2) => {
|
||||
const list2 = res2.data || [];
|
||||
const d2 = pickBestOrgQuantityRow(list2);
|
||||
if (d2 && Number(d2.orgQuantity ?? 0) > 0) {
|
||||
if (d2) {
|
||||
applyFromDto(d2, true);
|
||||
proxy.$message.info(
|
||||
'所选批号在本仓库无对应库存或批号不一致,已按仓库实物回显批号与可领数量,请核对。'
|
||||
);
|
||||
if (Number(r.totalQuantity) <= 0) {
|
||||
proxy.$message.warning('仓库数量为0,无法调用!');
|
||||
} else {
|
||||
proxy.$message.info(
|
||||
'所选批号在本仓库无对应库存或批号不一致,已按仓库实物回显批号与可领数量,请核对。'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
r.totalQuantity = 0;
|
||||
r.price = 0;
|
||||
|
||||
Reference in New Issue
Block a user