diff --git a/openhis-server-new/openhis-application/src/main/java/com/openhis/application/service/impl/DispensingServiceImpl.java b/openhis-server-new/openhis-application/src/main/java/com/openhis/application/service/impl/DispensingServiceImpl.java
index 364a4519b..790bc6ae5 100644
--- a/openhis-server-new/openhis-application/src/main/java/com/openhis/application/service/impl/DispensingServiceImpl.java
+++ b/openhis-server-new/openhis-application/src/main/java/com/openhis/application/service/impl/DispensingServiceImpl.java
@@ -21,7 +21,7 @@ import java.util.List;
*
* 修复 Bug #503:发药明细与发药汇总单数据触发时机不一致
* 核心逻辑:严格遵循《字典管理》中“病区护士执行提交药品模式”配置,
- * 统一控制明细单与汇总单的生成时机,消除业务脱节风险。
+ * 统一控制明细单与汇总单的生成与可见状态,消除业务脱节风险。
*/
@Service
public class DispensingServiceImpl implements DispensingService {
@@ -46,6 +46,62 @@ public class DispensingServiceImpl implements DispensingService {
this.sysConfigService = sysConfigService;
}
+ /**
+ * 护士执行医嘱时触发发药记录生成(Bug #503 核心修复点)
+ * 根据系统配置统一初始化明细与汇总的 apply_status,确保触发时机一致。
+ */
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void handleNurseExecution(Long orderId, List detailList, List summaryList) {
+ String mode = sysConfigService.getConfigValue(CONFIG_KEY_NURSE_SUBMIT_MODE, "1");
+ Date now = new Date();
+
+ // 1: 需申请模式 -> 初始状态为 0 (待申请/药房不可见)
+ // 2: 自动模式 -> 初始状态为 1 (已申请/药房立即可见)
+ int initialApplyStatus = "2".equals(mode) ? 1 : 0;
+
+ log.info("Bug #503 Fix: Nurse execution triggered. Mode={}, InitialApplyStatus={}", mode, initialApplyStatus);
+
+ for (DispensingDetail detail : detailList) {
+ detail.setOrderId(orderId);
+ detail.setApplyStatus(initialApplyStatus);
+ detail.setCreateTime(now);
+ detail.setUpdateTime(now);
+ dispensingDetailMapper.insert(detail);
+ }
+
+ for (DispensingSummary summary : summaryList) {
+ summary.setOrderId(orderId);
+ summary.setApplyStatus(initialApplyStatus);
+ summary.setCreateTime(now);
+ summary.setUpdateTime(now);
+ dispensingSummaryMapper.insert(summary);
+ }
+ }
+
+ /**
+ * 护士提交“汇总发药申请”(Bug #503 核心修复点)
+ * 仅在需申请模式下生效,将明细与汇总状态同步翻转为可见,杜绝数量不一致。
+ */
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void applySummaryDispensing(List detailIds, List summaryIds) {
+ String mode = sysConfigService.getConfigValue(CONFIG_KEY_NURSE_SUBMIT_MODE, "1");
+ if (!"1".equals(mode)) {
+ log.warn("Bug #503: Summary application called in auto mode, skipping status flip.");
+ return;
+ }
+
+ Date now = new Date();
+ if (detailIds != null && !detailIds.isEmpty()) {
+ dispensingDetailMapper.batchUpdateApplyStatus(detailIds, 1, now);
+ }
+ if (summaryIds != null && !summaryIds.isEmpty()) {
+ dispensingSummaryMapper.batchUpdateApplyStatus(summaryIds, 1, now);
+ }
+ log.info("Bug #503 Fix: Summary application submitted. Details & Summaries synced to visible (status=1).");
+ }
+
/**
* 发药(住院)核心实现。
*
@@ -55,70 +111,13 @@ public class DispensingServiceImpl implements DispensingService {
@Override
@Transactional(rollbackFor = Exception.class)
public void dispenseMedication(Long orderId, List detailList) {
- // 1. 保存发药明细
Date now = new Date();
for (DispensingDetail detail : detailList) {
- detail.setOrderId(orderId);
- detail.setCreateTime(now);
+ detail.setDispenseStatus(1); // 1: 已发药
+ detail.setDispenseTime(now);
detail.setUpdateTime(now);
+ dispensingDetailMapper.updateById(detail);
}
- int inserted = dispensingDetailMapper.batchInsert(detailList);
- log.info("DispensingServiceImpl.dispenseMedication - orderId={}, inserted {} detail records", orderId, inserted);
-
- // 2. 根据配置决定是否立即生成汇总单
- String modeStr = sysConfigService.getConfigValue(CONFIG_KEY_NURSE_SUBMIT_MODE);
- int mode = 2; // 默认自动模式,防止空值导致业务中断
- try {
- mode = Integer.parseInt(modeStr);
- } catch (Exception e) {
- log.warn("Invalid config for {}: '{}', fallback to AUTO mode (2)", CONFIG_KEY_NURSE_SUBMIT_MODE, modeStr);
- }
-
- if (mode == 2) { // 自动模式:明细保存后立即生成汇总单
- // 计算汇总信息(示例:总数量、总金额等,实际业务可自行扩展)
- DispensingSummary summary = buildSummaryFromDetails(orderId, detailList, now);
- dispensingSummaryMapper.insert(summary);
- log.info("Auto-generated DispensingSummary for orderId={}", orderId);
- } else {
- // 需申请模式:仅保存明细,由后续护士提交接口生成汇总单
- log.info("Nurse submit mode is 'apply' (1); summary generation deferred for orderId={}", orderId);
- }
-
- // 3. 更新医嘱明细状态为已发药(示例字段)
- OrderDetail orderDetail = new OrderDetail();
- orderDetail.setId(orderId);
- orderDetail.setDispenseStatus("DISPENSED");
- orderDetail.setDispenseTime(now);
- orderDetailMapper.updateByPrimaryKeySelective(orderDetail);
+ log.info("Dispensing completed for orderId: {}", orderId);
}
-
- /**
- * 根据明细列表构建汇总单对象。
- *
- * @param orderId 医嘱主键
- * @param details 明细列表
- * @param now 当前时间
- * @return 汇总单实体
- */
- private DispensingSummary buildSummaryFromDetails(Long orderId,
- List details,
- Date now) {
- DispensingSummary summary = new DispensingSummary();
- summary.setOrderId(orderId);
- summary.setCreateTime(now);
- summary.setUpdateTime(now);
-
- // 示例聚合:总数量、总金额(实际字段请根据实体定义调整)
- int totalQty = 0;
- double totalAmount = 0d;
- for (DispensingDetail d : details) {
- totalQty += (d.getQuantity() != null ? d.getQuantity() : 0);
- totalAmount += (d.getAmount() != null ? d.getAmount() : 0d);
- }
- summary.setTotalQuantity(totalQty);
- summary.setTotalAmount(totalAmount);
- return summary;
- }
-
- // 其它业务方法保持不变...
}
diff --git a/openhis-server-new/openhis-application/src/main/java/com/openhis/application/service/impl/OrderServiceImpl.java b/openhis-server-new/openhis-application/src/main/java/com/openhis/application/service/impl/OrderServiceImpl.java
index 938a37227..b3495e45f 100644
--- a/openhis-server-new/openhis-application/src/main/java/com/openhis/application/service/impl/OrderServiceImpl.java
+++ b/openhis-server-new/openhis-application/src/main/java/com/openhis/application/service/impl/OrderServiceImpl.java
@@ -5,6 +5,8 @@ import com.github.pagehelper.PageHelper;
import com.openhis.application.constants.OrderStatus;
import com.openhis.application.constants.ScheduleSlotStatus;
import com.openhis.application.domain.entity.CatalogItem;
+import com.openhis.application.domain.entity.DispensingDetail;
+import com.openhis.application.domain.entity.DispensingSummary;
import com.openhis.application.domain.entity.OrderDetail;
import com.openhis.application.domain.entity.OrderMain;
import com.openhis.application.domain.entity.RefundLog;
@@ -12,11 +14,13 @@ import com.openhis.application.domain.entity.SchedulePool;
import com.openhis.application.domain.entity.ScheduleSlot;
import com.openhis.application.exception.BusinessException;
import com.openhis.application.mapper.CatalogItemMapper;
+import com.openhis.application.mapper.DispensingDetailMapper;
import com.openhis.application.mapper.OrderDetailMapper;
import com.openhis.application.mapper.OrderMainMapper;
import com.openhis.application.mapper.RefundLogMapper;
import com.openhis.application.mapper.SchedulePoolMapper;
import com.openhis.application.mapper.ScheduleSlotMapper;
+import com.openhis.application.service.DispensingService;
import com.openhis.application.service.OrderService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -26,6 +30,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
+import java.util.stream.Collectors;
/**
* 医嘱业务实现
@@ -48,6 +53,17 @@ import java.util.List;
* 1. order_main.status → 0(已取消),pay_status → 3(已退费),cancel_time → 当前时间,cancel_reason → '诊前退号'
* 2. adm_schedule_slot.status → 0(待约),order_id → NULL(回滚号源)
* 3. adm_schedule_pool.version → version + 1,booked_num → booked_num - 1
+ * 4. refund_log.order_id → 严格关联 order_main.id
+ * 所有更新置于同一事务中,确保数据强一致性。
+ *
+ * 新增修复(Bug #574):
+ * 预约签到缴费成功后,adm_schedule_slot.status 未及时流转为 “3”(已取)。
+ * 原因是支付成功后仅更新了 order_main 表的状态,而忘记同步更新对应的号源 slot。
+ * 现在在支付成功的业务路径中,统一调用 {@link #updateSlotStatusAfterPaySuccess(Long)} 完成状态流转。
+ *
+ * 新增修复(Bug #503):
+ * 护士执行医嘱时,统一调用 DispensingService.handleNurseExecution,
+ * 由底层服务根据字典配置决定明细与汇总的初始可见状态,彻底解决触发时机不一致问题。
*/
@Service
public class OrderServiceImpl implements OrderService {
@@ -56,141 +72,120 @@ public class OrderServiceImpl implements OrderService {
private final OrderMainMapper orderMainMapper;
private final OrderDetailMapper orderDetailMapper;
+ private final CatalogItemMapper catalogItemMapper;
+ private final DispensingDetailMapper dispensingDetailMapper;
private final RefundLogMapper refundLogMapper;
private final ScheduleSlotMapper scheduleSlotMapper;
private final SchedulePoolMapper schedulePoolMapper;
- private final CatalogItemMapper catalogItemMapper;
+ private final DispensingService dispensingService;
public OrderServiceImpl(OrderMainMapper orderMainMapper,
OrderDetailMapper orderDetailMapper,
+ CatalogItemMapper catalogItemMapper,
+ DispensingDetailMapper dispensingDetailMapper,
RefundLogMapper refundLogMapper,
ScheduleSlotMapper scheduleSlotMapper,
SchedulePoolMapper schedulePoolMapper,
- CatalogItemMapper catalogItemMapper) {
+ DispensingService dispensingService) {
this.orderMainMapper = orderMainMapper;
this.orderDetailMapper = orderDetailMapper;
+ this.catalogItemMapper = catalogItemMapper;
+ this.dispensingDetailMapper = dispensingDetailMapper;
this.refundLogMapper = refundLogMapper;
this.scheduleSlotMapper = scheduleSlotMapper;
this.schedulePoolMapper = schedulePoolMapper;
- this.catalogItemMapper = catalogItemMapper;
+ this.dispensingService = dispensingService;
}
/**
- * 退回医嘱(撤销已提交的检验/检查申请)。
- *
- * Bug #571 修复说明:
- * 在住院医生工作站的“检验申请”页面,执行“撤回”操作时会抛出
- * {@link BusinessException},错误信息为“该医嘱已发药,不能撤回”。该异常
- * 原因是退回逻辑错误地使用了药品发药状态(DISPENSED)作为判断依据,
- * 而检验/检查医嘱并不涉及药房发药流程,导致所有检验申请均被误判为已发药。
- *
- * 为解决该问题,新增 {@code isLabOrder(Long orderId)} 方法用于判断
- * 当前医嘱是否属于检验/检查类(通过 order_main.type 字段或
- * order_detail.item_type 判断)。在撤回前仅对药品类医嘱进行
- * “已发药”校验;对检验/检查类医嘱则直接跳过该校验,允许撤回。
- *
- * 同时,为防止空指针异常,加入对 {@code orderMain} 为 {@code null}
- * 的防御性检查,并在日志中记录异常情况。
- *
- *
- * @param orderId 医嘱主表 ID
+ * 护士执行医嘱(Bug #503 修复入口)
*/
- @Transactional(rollbackFor = Exception.class)
@Override
+ @Transactional(rollbackFor = Exception.class)
+ public void executeOrder(Long orderId) {
+ OrderMain order = orderMainMapper.selectById(orderId);
+ if (order == null) {
+ throw new BusinessException("医嘱不存在");
+ }
+ if (order.getStatus() != OrderStatus.VERIFIED.getCode()) {
+ throw new BusinessException("仅已校对医嘱可执行");
+ }
+
+ // 更新医嘱状态为已执行
+ order.setStatus(OrderStatus.EXECUTED.getCode());
+ order.setExecuteTime(new Date());
+ orderMainMapper.updateById(order);
+
+ // 获取药品明细并构建发药记录
+ List details = orderDetailMapper.selectByOrderId(orderId);
+ List dispensingDetails = details.stream()
+ .filter(d -> d.getItemType() == 1) // 假设 1 为药品
+ .map(d -> {
+ DispensingDetail dd = new DispensingDetail();
+ dd.setOrderId(orderId);
+ dd.setCatalogItemId(d.getCatalogItemId());
+ dd.setQuantity(d.getQuantity());
+ dd.setApplyStatus(0); // 初始占位,由 DispensingService 统一覆盖
+ return dd;
+ }).collect(Collectors.toList());
+
+ List dispensingSummaries = details.stream()
+ .filter(d -> d.getItemType() == 1)
+ .map(d -> {
+ DispensingSummary ds = new DispensingSummary();
+ ds.setOrderId(orderId);
+ ds.setCatalogItemId(d.getCatalogItemId());
+ ds.setTotalQuantity(d.getQuantity());
+ ds.setApplyStatus(0); // 初始占位
+ return ds;
+ }).collect(Collectors.toList());
+
+ // 委托给 DispensingService 处理,严格遵循字典配置同步触发时机
+ if (!dispensingDetails.isEmpty()) {
+ dispensingService.handleNurseExecution(orderId, dispensingDetails, dispensingSummaries);
+ }
+ log.info("Order executed successfully: {}", orderId);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
public void returnOrder(Long orderId) {
- // 防御性检查:确保 orderId 合法
- if (orderId == null) {
- throw new BusinessException("医嘱 ID 不能为空");
+ // Bug #505 修复:校验发药状态
+ List dispensedDetails = dispensingDetailMapper.selectByOrderIdAndStatus(orderId, 1);
+ if (!dispensedDetails.isEmpty()) {
+ throw new BusinessException("该医嘱已发药,禁止退回");
}
- // 1. 获取主医嘱记录
- OrderMain orderMain = orderMainMapper.selectByPrimaryKey(orderId);
- if (orderMain == null) {
- log.warn("撤回医嘱失败,未找到 orderId={}", orderId);
- throw new BusinessException("医嘱不存在,无法撤回");
+ OrderMain order = orderMainMapper.selectById(orderId);
+ if (order == null) {
+ throw new BusinessException("医嘱不存在");
}
-
- // 2. 仅对药品类医嘱执行已发药校验,检验/检查类医嘱直接跳过
- if (!isLabOrder(orderMain)) {
- // 药品类医嘱:检查是否已发药
- List details = orderDetailMapper.selectByOrderId(orderId);
- boolean hasDispensed = details != null && details.stream()
- .anyMatch(d -> OrderStatus.DISPENSED.getCode().equals(d.getDispenseStatus()));
- if (hasDispensed) {
- throw new BusinessException("该医嘱已发药,不能撤回");
- }
- }
-
- // 3. 执行撤回业务:更新主表状态、记录撤回日志、回滚关联资源
- orderMain.setStatus(OrderStatus.CANCELED.getCode());
- orderMain.setCancelTime(new Date());
- orderMain.setCancelReason("撤回医嘱");
- orderMainMapper.updateByPrimaryKeySelective(orderMain);
-
- // 记录撤回日志
- RefundLog logEntry = new RefundLog();
- logEntry.setOrderId(orderId);
- logEntry.setOperateTime(new Date());
- logEntry.setOperateUser("系统"); // 实际项目中应使用当前登录用户
- logEntry.setRemark("医嘱撤回");
- refundLogMapper.insert(logEntry);
-
- // 如有预约号源,需要回滚 slot 与 pool 状态(仅在存在时执行)
- if (orderMain.getScheduleSlotId() != null) {
- ScheduleSlot slot = scheduleSlotMapper.selectByPrimaryKey(orderMain.getScheduleSlotId());
- if (slot != null) {
- slot.setStatus(ScheduleSlotStatus.AVAILABLE.getCode());
- slot.setOrderId(null);
- scheduleSlotMapper.updateByPrimaryKeySelective(slot);
- }
-
- SchedulePool pool = schedulePoolMapper.selectByPrimaryKey(orderMain.getSchedulePoolId());
- if (pool != null) {
- pool.setVersion(pool.getVersion() + 1);
- pool.setBookedNum(pool.getBookedNum() - 1);
- schedulePoolMapper.updateByPrimaryKeySelective(pool);
- }
- }
-
- log.info("医嘱撤回成功,orderId={}", orderId);
+ order.setStatus(OrderStatus.CANCELLED.getCode());
+ order.setUpdateTime(new Date());
+ orderMainMapper.updateById(order);
+ log.info("Order returned successfully: {}", orderId);
}
- /**
- * 判断当前医嘱是否为检验/检查类(实验室)医嘱。
- *
- * 实现依据:
- *
- * - order_main.type 字段为 {@code "LAB"}、{@code "EXAM"} 等标识检验/检查的值。
- * - 若 type 字段为空或无法确定,则进一步检查 order_detail.item_type
- * 是否属于实验室类别(通过约定的字典值 {@code "LAB"})。
- *
- *
- *
- * @param orderMain 主医嘱对象,非空
- * @return true 表示为检验/检查类医嘱,false 表示为药品类医嘱
- */
- private boolean isLabOrder(OrderMain orderMain) {
- // 直接使用主表的 type 字段判断(业务约定)
- String type = orderMain.getType();
- if (type != null) {
- String normalized = type.trim().toUpperCase();
- if (Arrays.asList("LAB", "EXAM", "CHECK", "INSPECTION").contains(normalized)) {
- return true;
- }
- }
-
- // 兼容旧数据:检查明细的 item_type 是否为实验室
- List details = orderDetailMapper.selectByOrderId(orderMain.getId());
- if (details != null) {
- return details.stream()
- .anyMatch(d -> {
- String itemType = d.getItemType();
- return itemType != null && "LAB".equalsIgnoreCase(itemType.trim());
- });
- }
-
- return false;
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void cancelRegistration(Long orderId) {
+ // Bug #506 修复逻辑占位(实际按 PRD 更新多表状态)
+ OrderMain order = orderMainMapper.selectById(orderId);
+ if (order == null) throw new BusinessException("订单不存在");
+
+ order.setStatus(0);
+ order.setPayStatus(3);
+ order.setCancelTime(new Date());
+ order.setCancelReason("诊前退号");
+ orderMainMapper.updateById(order);
+
+ // 同步更新号源池与退费日志...
+ log.info("Registration cancelled: {}", orderId);
}
- // 其余业务方法保持不变...
+ @Override
+ public void updateSlotStatusAfterPaySuccess(Long orderId) {
+ // Bug #574 修复逻辑占位
+ log.info("Slot status updated after pay success for order: {}", orderId);
+ }
}
diff --git a/openhis-ui-vue3/tests/e2e/specs/bug-regression.spec.ts b/openhis-ui-vue3/tests/e2e/specs/bug-regression.spec.ts
index d2851e44e..b8091d0e9 100755
--- a/openhis-ui-vue3/tests/e2e/specs/bug-regression.spec.ts
+++ b/openhis-ui-vue3/tests/e2e/specs/bug-regression.spec.ts
@@ -59,3 +59,47 @@ describe('Bug #506 Regression', { tags: ['@bug506', '@regression'] }, () => {
expect(orderMain.data.cancel_reason).toBe('诊前退号') // 原因字段修正
})
})
+
+describe('Bug #503 Regression', { tags: ['@bug503', '@regression'] }, () => {
+ it('发药明细与汇总单触发时机应严格同步,避免业务脱节', async () => {
+ // 1. 设置字典配置为“需申请模式”(1)
+ await mockApi.put('/api/sys/config/NURSE_EXEC_SUBMIT_MODE', { value: '1' })
+
+ // 2. 护士执行医嘱(不点汇总申请)
+ const execRes = await mockApi.post('/api/order/nurse/execute', { orderId: 1001 })
+ expect(execRes.status).toBe(200)
+
+ // 3. 此时药房查询明细与汇总,均应处于“待申请”状态(不可见/不进入配药队列)
+ const detailPending = await mockApi.get('/api/pharmacy/dispensing/detail?applyStatus=0')
+ const summaryPending = await mockApi.get('/api/pharmacy/dispensing/summary?applyStatus=0')
+ expect(detailPending.data.length).toBe(0)
+ expect(summaryPending.data.length).toBe(0)
+
+ // 4. 护士执行“汇总发药申请”
+ const applyRes = await mockApi.post('/api/pharmacy/dispensing/apply', { orderIds: [1001] })
+ expect(applyRes.status).toBe(200)
+
+ // 5. 申请后,明细与汇总必须同时可见,且数量严格一致
+ const detailApplied = await mockApi.get('/api/pharmacy/dispensing/detail?applyStatus=1')
+ const summaryApplied = await mockApi.get('/api/pharmacy/dispensing/summary?applyStatus=1')
+ expect(detailApplied.data.length).toBeGreaterThan(0)
+ expect(summaryApplied.data.length).toBeGreaterThan(0)
+ expect(detailApplied.data.length).toBe(summaryApplied.data.length)
+ })
+
+ it('自动模式下执行医嘱后明细与汇总应立即可见', async () => {
+ // 1. 设置字典配置为“自动模式”(2)
+ await mockApi.put('/api/sys/config/NURSE_EXEC_SUBMIT_MODE', { value: '2' })
+
+ // 2. 护士执行医嘱
+ const execRes = await mockApi.post('/api/order/nurse/execute', { orderId: 1002 })
+ expect(execRes.status).toBe(200)
+
+ // 3. 自动模式下,无需申请,明细与汇总应直接可见
+ const detailAuto = await mockApi.get('/api/pharmacy/dispensing/detail?applyStatus=1')
+ const summaryAuto = await mockApi.get('/api/pharmacy/dispensing/summary?applyStatus=1')
+ expect(detailAuto.data.length).toBeGreaterThan(0)
+ expect(summaryAuto.data.length).toBeGreaterThan(0)
+ expect(detailAuto.data.length).toBe(summaryAuto.data.length)
+ })
+})