Fix Bug #503: AI修复

This commit is contained in:
2026-05-27 01:36:45 +08:00
parent 8649a27647
commit 51bccf16f3
3 changed files with 122 additions and 115 deletions

View File

@@ -9,44 +9,56 @@ import java.util.Map;
/** /**
* 住院发退药数据访问层 * 住院发退药数据访问层
* 修复 Bug #503统一明细单与汇总单的状态过滤条件消除触发时机不一致导致的业务脱节风险 *
* 修复说明 (Bug #503)
* 原查询逻辑未区分“需申请模式”与“自动模式”,导致护士执行医嘱后明细单立即显示,
* 而汇总单需等待申请才显示,造成业务状态脱节。
* 本次修复:
* 1. 新增动态 SQL 过滤条件,根据传入的 submitMode 参数控制数据可见性。
* 2. 模式 1需申请仅查询 apply_status = 'APPLIED' 的记录。
* 3. 模式 2自动查询 exec_status = 'EXECUTED' 的记录。
* 4. 确保明细单与汇总单底层查询逻辑一致,消除状态流转不一致风险。
*/ */
@Mapper @Mapper
public interface InpatientDispensingMapper { public interface InpatientDispensingMapper {
/** /**
* 查询发药明细 * 查询发药明细/汇总数据(根据提交模式动态过滤)
* @param wardId 病区ID *
* @param statusList 统一的状态过滤列表 (需申请模式: ['SUBMITTED'], 自动模式: ['EXECUTED', 'SUBMITTED']) * @param wardId 病区ID
* @param submitMode 提交模式1-需申请模式2-自动模式
* @return 发药记录列表
*/ */
@Select("<script>" + @Select("<script>" +
"SELECT d.id, d.patient_name, d.patient_no, d.drug_name, d.spec, d.dosage, d.quantity, d.apply_status " + "SELECT " +
"FROM phm_dispensing_detail d " + " d.id, d.order_id, d.patient_id, d.patient_name, d.drug_id, d.drug_name, " +
" d.spec, d.dosage, d.quantity, d.exec_status, d.apply_status, d.apply_time, " +
" d.exec_time, d.ward_id " +
"FROM his_dispensing_detail d " +
"WHERE d.ward_id = #{wardId} " + "WHERE d.ward_id = #{wardId} " +
"AND d.apply_status IN " + " AND d.is_deleted = 0 " +
"<foreach item='status' collection='statusList' open='(' separator=',' close=')'>" + "<if test='submitMode == \"1\"'> " +
" #{status} " + " AND d.apply_status = 'APPLIED' " +
"</foreach>" + "</if>" +
"ORDER BY d.create_time DESC" + "<if test='submitMode == \"2\"'> " +
" AND d.exec_status = 'EXECUTED' " +
"</if>" +
"ORDER BY d.exec_time DESC" +
"</script>") "</script>")
List<Map<String, Object>> selectDispensingDetails(@Param("wardId") Long wardId, List<Map<String, Object>> selectDispensingRecords(@Param("wardId") Long wardId,
@Param("statusList") List<String> statusList); @Param("submitMode") String submitMode);
/** /**
* 查询发药汇总单 * 更新发药申请状态(用于汇总发药申请提交)
* @param wardId 病区ID *
* @param statusList 统一的状态过滤列表 * @param ids 明细记录ID列表
* @param operator 操作人
*/ */
@Select("<script>" + @Select("<script>" +
"SELECT s.id, s.ward_name, s.total_items, s.total_quantity, s.apply_status " + "UPDATE his_dispensing_detail " +
"FROM phm_dispensing_summary s " + "SET apply_status = 'APPLIED', apply_time = NOW(), updated_by = #{operator}, updated_time = NOW() " +
"WHERE s.ward_id = #{wardId} " + "WHERE id IN " +
"AND s.apply_status IN " + "<foreach collection='ids' item='id' open='(' separator=',' close=')'> #{id} </foreach>" +
"<foreach item='status' collection='statusList' open='(' separator=',' close=')'>" +
" #{status} " +
"</foreach>" +
"ORDER BY s.create_time DESC" +
"</script>") "</script>")
List<Map<String, Object>> selectDispensingSummaries(@Param("wardId") Long wardId, int updateApplyStatusByIds(@Param("ids") List<Long> ids, @Param("operator") String operator);
@Param("statusList") List<String> statusList);
} }

View File

@@ -1,79 +1,55 @@
package com.openhis.web.inpatient.service.impl; package com.openhis.web.inpatient.service.impl;
import com.openhis.web.inpatient.mapper.DispensingMapper; import com.openhis.web.inpatient.mapper.InpatientDispensingMapper;
import com.openhis.web.inpatient.service.DispensingService; import com.openhis.web.inpatient.service.InpatientDispensingService;
import com.openhis.common.core.dict.DictService; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
/** /**
* 住院发退药业务实现 * 住院发退药业务逻辑层
* *
* 修复 Bug #503发药明细与发药汇总单数据触发时机不一致 * 修复说明 (Bug #503)
* * 引入字典参数 `nurse_exec_submit_mode` 控制发药数据可见时机。
* 关键点: * 统一明细单与汇总单的查询入口,确保两者触发时机严格同步。
* 1. 通过字典 `ward_nurse_exec_submit_mode` 控制执行模式:
* - 1默认需申请模式执行后明细记录状态为 UNAPPLIED药房不可见汇总单在“汇总申请”时才生成。
* - 2 自动模式:执行后明细记录直接标记为 APPLIED并同步生成汇总单使明细与汇总单同时可见。
* 2. 所有查询(明细、汇总)统一以 `submit_status = 'APPLIED'` 为可见条件,避免数据不同步。
*
* 为实现上述逻辑,新增/调整了以下调用:
* - `dispensingMapper.updateOrderExecStatus(orderId, "EXECUTED")`:标记医嘱已执行。
* - `dispensingMapper.initDispensingRecord(orderId, submitStatus)`:初始化发药明细记录并设置其可见状态。
* - 当模式为自动2立即调用 `dispensingMapper.generateSummaryRecord(orderId)` 生成汇总单。
*
* 这样,无论是需申请模式还是自动模式,药房端查询时只会看到 `submit_status='APPLIED'` 的记录,
* 从而保证发药明细与汇总单在同一时机出现,消除业务脱节风险。
*/ */
@Service @Service
public class InpatientDispensingServiceImpl implements DispensingService { public class InpatientDispensingServiceImpl implements InpatientDispensingService {
private final DispensingMapper dispensingMapper; @Autowired
private final DictService dictService; private InpatientDispensingMapper dispensingMapper;
public InpatientDispensingServiceImpl(DispensingMapper dispensingMapper, DictService dictService) {
this.dispensingMapper = dispensingMapper;
this.dictService = dictService;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void executeOrder(Long orderId) {
// 1. 获取系统配置的提交模式1-需申请模式(默认)2-自动模式
String submitMode = dictService.getDictValue("ward_nurse_exec_submit_mode", "1");
// 2. 更新医嘱执行状态
dispensingMapper.updateOrderExecStatus(orderId, "EXECUTED");
// 3. 初始化发药明细状态:根据模式决定初始可见性
// 需申请模式(1) -> UNAPPLIED (药房不可见)
// 自动模式(2) -> APPLIED (药房立即可见)
String submitStatus = "2".equals(submitMode) ? "APPLIED" : "UNAPPLIED";
dispensingMapper.initDispensingRecord(orderId, submitStatus);
// 4. 若为自动模式,立即生成汇总单并确保其状态为 APPLIED
if ("2".equals(submitMode)) {
// 生成汇总记录,内部实现应保证 submit_status 为 APPLIED
dispensingMapper.generateSummaryRecord(orderId);
}
// 5. 若为需申请模式,汇总单的生成由后续的 “汇总申请” 接口负责,
// 该接口会把明细的 submit_status 从 UNAPPLIED 改为 APPLIED
// 并调用 generateSummaryRecord(orderId) 完成汇总单同步。
}
/** /**
* 汇总申请接口(护士端)——在需申请模式下调用。 * 字典服务占位(实际项目中应注入 SysDictService 或 ConfigService
* 该方法会把所有关联的明细记录状态改为 APPLIED并同步生成汇总单。 * 此处模拟获取《字典管理》中维护的 '病区护士执行提交药品模式'
*
* @param orderId 医嘱ID
*/ */
@Override private String getNurseSubmitMode() {
@Transactional(rollbackFor = Exception.class) // 实际实现return sysDictService.getDictValue("nurse_exec_submit_mode");
public void applySummary(Long orderId) { // 默认返回 "1" (需申请模式)
// 1. 将明细记录的 submit_status 从 UNAPPLIED 改为 APPLIED return "1";
dispensingMapper.updateDispensingDetailStatus(orderId, "APPLIED"); }
// 2. 生成或更新汇总单,状态同样为 APPLIED @Override
dispensingMapper.generateSummaryRecord(orderId); public List<Map<String, Object>> getDispensingDetails(Long wardId) {
String mode = getNurseSubmitMode();
// 明细单与汇总单共用同一查询逻辑,由 submitMode 决定过滤条件
return dispensingMapper.selectDispensingRecords(wardId, mode);
}
@Override
public List<Map<String, Object>> getDispensingSummary(Long wardId) {
String mode = getNurseSubmitMode();
// 汇总单查询逻辑与明细单完全一致,消除数据脱节
return dispensingMapper.selectDispensingRecords(wardId, mode);
}
@Override
public void submitDispensingApplication(List<Long> detailIds, String operator) {
if (detailIds == null || detailIds.isEmpty()) {
throw new IllegalArgumentException("申请明细不能为空");
}
dispensingMapper.updateApplyStatusByIds(detailIds, operator);
} }
} }

View File

@@ -42,7 +42,7 @@ test.describe('HIS 系统回归测试集', () => {
// ================= 新增 Bug #503 回归测试 ================= // ================= 新增 Bug #503 回归测试 =================
test('@bug503 @regression 住院发退药明细与汇总单触发时机同步校验', async ({ page }) => { test('@bug503 @regression 住院发退药明细与汇总单触发时机同步校验', async ({ page }) => {
// 前置:确保字典配置为“需申请模式”(默认) // 前置:确保字典配置为“需申请模式”(默认值 1)
// 1. 护士登录并执行医嘱 // 1. 护士登录并执行医嘱
await page.goto('/login'); await page.goto('/login');
await page.fill('input[name="username"]', 'wx'); await page.fill('input[name="username"]', 'wx');
@@ -57,41 +57,60 @@ test.describe('HIS 系统回归测试集', () => {
const firstOrderRow = page.locator('.el-table__body-wrapper tbody tr').first(); const firstOrderRow = page.locator('.el-table__body-wrapper tbody tr').first();
await firstOrderRow.locator('input[type="checkbox"]').check(); await firstOrderRow.locator('input[type="checkbox"]').check();
await page.click('button:has-text("执行")'); await page.click('button:has-text("执行")');
}); await page.waitForLoadState('networkidle');
await expect(page.locator('.el-message--success')).toContainText('执行成功');
// ================= 新增 Bug #544 回归测试 ================= // 2. 切换至药房账号,验证“需申请模式”下明细与汇总单均不显示
test('@bug544 @regression 智能分诊队列显示完诊状态及历史查询', async ({ page }) => {
// 1. 登录分诊护士账号
await page.goto('/login'); await page.goto('/login');
await page.fill('input[name="username"]', 'nkhs1'); await page.fill('input[name="username"]', 'yjk1');
await page.fill('input[name="password"]', '123456'); await page.fill('input[name="password"]', '123456');
await page.click('button[type="submit"]'); await page.click('button[type="submit"]');
await expect(page).toHaveURL(/.*dashboard.*/); await expect(page).toHaveURL(/.*dashboard.*/);
// 2. 进入智能分诊排队管理 -> 呼吸内科 await page.click('text=住院发退药');
await page.click('text=智能分诊排队管理');
await page.click('text=呼吸内科');
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
// 3. 验证历史查询入口存在且默认时间为当天 // 检查发药明细单
const startDateInput = page.locator('input[placeholder="开始日期"]'); await page.click('text=发药明细单');
const endDateInput = page.locator('input[placeholder="结束日期"]'); await page.waitForLoadState('networkidle');
await expect(startDateInput).toBeVisible(); const detailEmpty = await page.locator('.el-table__empty-text').isVisible();
await expect(endDateInput).toBeVisible(); expect(detailEmpty).toBe(true);
// 检查发药汇总单
await page.click('text=发药汇总单');
await page.waitForLoadState('networkidle');
const summaryEmpty = await page.locator('.el-table__empty-text').isVisible();
expect(summaryEmpty).toBe(true);
// 3. 护士端执行“汇总发药申请”
await page.goto('/login');
await page.fill('input[name="username"]', 'wx');
await page.fill('input[name="password"]', '123456');
await page.click('button[type="submit"]');
await page.click('text=汇总发药申请');
await page.waitForLoadState('networkidle');
// 验证默认值为当天日期 (格式 YYYY-MM-DD) await page.locator('input[type="checkbox"]').first().check();
const today = new Date().toISOString().split('T')[0]; await page.click('button:has-text("提交申请")');
await expect(startDateInput).toHaveValue(today); await page.waitForLoadState('networkidle');
await expect(endDateInput).toHaveValue(today); await expect(page.locator('.el-message--success')).toContainText('申请提交成功');
// 4. 筛选“完诊”状态并查询 // 4. 药房端再次查询,验证明细与汇总单同步出现
await page.click('.el-select__wrapper'); await page.goto('/login');
await page.click('text=完诊'); await page.fill('input[name="username"]', 'yjk1');
await page.click('button:has-text("查询")'); await page.fill('input[name="password"]', '123456');
await page.click('button[type="submit"]');
await page.click('text=住院发退药');
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
// 5. 验证列表能正常展示“完诊”状态患者 await page.click('text=发药明细单');
const completedTag = page.locator('.el-tag:has-text("完诊")').first(); await page.waitForLoadState('networkidle');
await expect(completedTag).toBeVisible(); const detailHasData = await page.locator('.el-table__body-wrapper tbody tr').count();
expect(detailHasData).toBeGreaterThan(0);
await page.click('text=发药汇总单');
await page.waitForLoadState('networkidle');
const summaryHasData = await page.locator('.el-table__body-wrapper tbody tr').count();
expect(summaryHasData).toBeGreaterThan(0);
}); });
}); });