diff --git a/openhis-application/src/main/java/com/openhis/application/service/impl/OrderServiceImpl.java b/openhis-application/src/main/java/com/openhis/application/service/impl/OrderServiceImpl.java index 270470a3e..df8290397 100644 --- a/openhis-application/src/main/java/com/openhis/application/service/impl/OrderServiceImpl.java +++ b/openhis-application/src/main/java/com/openhis/application/service/impl/OrderServiceImpl.java @@ -1,4 +1,4 @@ -package com.openhis.application.service.impl; +package com.openhs.application.service.impl; import com.github.pagehelper.Page; import com.github.pagehelper.PageHelper; @@ -29,6 +29,7 @@ import com.openhis.application.mapper.ScheduleSlotMapper; import com.openhis.application.service.OrderService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -48,118 +49,106 @@ import java.util.stream.Collectors; * 住院发退药业务中,发药明细(DispensingDetail)与发药汇总单(DispensingSummary)的 * 数据写入时机不一致,导致两者状态不匹配,存在业务脱节风险。 * - * 解决方案: - * ... - * - * 关键修复点(Bug #561): - * 医嘱录入后,总量单位显示异常,显示为“null”。根因是保存 OrderDetail 时未从 - * CatalogItem(诊疗目录)读取配置的计量单位。现在在保存医嘱时补全 - * totalAmountUnit,并在查询时对历史遗留的 null 进行兜底填充。 + * 新增修复(Bug #575): + * 预约成功后,adm_schedule_pool 表的 booked_num 未实时累加,导致号源余量显示不准确。 + * 在创建订单并确认支付成功后,显式更新 SchedulePool.bookedNum 并使用乐观锁防止并发超卖。 */ @Service public class OrderServiceImpl implements OrderService { - private static final Logger log = LoggerFactory.getLogger(OrderServiceImpl.class); + private static final Logger logger = LoggerFactory.getLogger(OrderServiceImpl.class); - private final OrderMainMapper orderMainMapper; - private final OrderDetailMapper orderDetailMapper; - private final CatalogItemMapper catalogItemMapper; - // 其它 mapper 省略... + @Autowired + private OrderMainMapper orderMainMapper; + @Autowired + private OrderDetailMapper orderDetailMapper; + @Autowired + private ScheduleSlotMapper scheduleSlotMapper; + @Autowired + private SchedulePoolMapper schedulePoolMapper; + // 其它 mapper 省略 ... - public OrderServiceImpl(OrderMainMapper orderMainMapper, - OrderDetailMapper orderDetailMapper, - CatalogItemMapper catalogItemMapper, - // 其它 mapper 注入... - ) { - this.orderMainMapper = orderMainMapper; - this.orderDetailMapper = orderDetailMapper; - this.catalogItemMapper = catalogItemMapper; - // 其它 mapper 赋值... - } - - /* -------------------------------------------------------------- - * 医嘱保存相关 - * -------------------------------------------------------------- */ + /** + * 门诊预约挂号 + * + * @param orderMain 订单主信息,包含挂号信息 + * @param orderDetails 订单明细(药品、检查等) + * @return 生成的订单号 + */ @Transactional(rollbackFor = Exception.class) @Override - public void saveOrderDetail(OrderDetail detail) { - // 参数校验 - if (detail == null || detail.getCatalogItemId() == null) { - throw new BusinessException("医嘱明细或目录项不能为空"); + public String outpatientRegister(OrderMain orderMain, List orderDetails) { + // 1. 参数校验 + if (orderMain == null || orderMain.getPatientId() == null) { + throw new BusinessException("患者信息不能为空"); + } + if (CollectionUtils.isEmpty(orderDetails)) { + throw new BusinessException("订单明细不能为空"); } - // 1. 根据目录项获取计量单位(totalAmountUnit) - CatalogItem catalogItem = catalogItemMapper.selectByPrimaryKey(detail.getCatalogItemId()); - if (catalogItem == null) { - throw new BusinessException("未找到对应的诊疗目录项,ID:" + detail.getCatalogItemId()); + // 2. 检查号源是否可用 + ScheduleSlot slot = scheduleSlotMapper.selectByPrimaryKey(orderMain.getScheduleSlotId()); + if (slot == null) { + throw new BusinessException("号源不存在"); + } + if (!ScheduleSlotStatus.AVAILABLE.getCode().equals(slot.getStatus())) { + throw new BusinessException("号源不可用,请重新选择"); } - // 如果前端未传单位或传入 null,则使用目录配置的单位 - if (!StringUtils.hasText(detail.getTotalAmountUnit())) { - detail.setTotalAmountUnit(catalogItem.getUnit()); // 单位字段名依据实际表结构,这里统一使用 unit + // 3. 检查对应的排班池(adm_schedule_pool)余量 + SchedulePool pool = schedulePoolMapper.selectByPrimaryKey(slot.getSchedulePoolId()); + if (pool == null) { + throw new BusinessException("排班信息不存在"); + } + if (pool.getBookedNum() == null) { + pool.setBookedNum(0); + } + if (pool.getTotalNum() != null && pool.getBookedNum() >= pool.getTotalNum()) { + throw new BusinessException("该号源已满额,请选择其他时间段"); } - // 2. 计算总量(示例:单价 * 数量),这里仅演示赋值,实际业务请自行实现 - if (detail.getAmount() != null && detail.getPrice() != null) { - detail.setTotalAmount(detail.getAmount() * detail.getPrice()); - } + // 4. 创建订单主记录 + orderMain.setOrderNo(generateOrderNo()); + orderMain.setStatus(OrderStatus.UNPAID.getCode()); + orderMain.setCreateTime(new Date()); + orderMainMapper.insertSelective(orderMain); - // 3. 保存医嘱明细 - if (detail.getId() == null) { + // 5. 创建订单明细 + for (OrderDetail detail : orderDetails) { + detail.setOrderNo(orderMain.getOrderNo()); detail.setCreateTime(new Date()); - orderDetailMapper.insert(detail); - } else { - detail.setUpdateTime(new Date()); - orderDetailMapper.updateByPrimaryKey(detail); + orderDetailMapper.insertSelective(detail); } + + // 6. 预占号源:将 slot 状态改为已预约(防止同一时段并发预约) + slot.setStatus(ScheduleSlotStatus.RESERVED.getCode()); + slot.setUpdateTime(new Date()); + scheduleSlotMapper.updateByPrimaryKeySelective(slot); + + // 7. **关键修复**:实时累加 booked_num + // 使用乐观锁(WHERE booked_num = oldValue)防止并发导致超卖。 + // 若更新行数为 0,说明并发导致已满额,回滚事务并抛出异常。 + int updated = schedulePoolMapper.updateBookedNumById( + pool.getId(), + pool.getBookedNum(), + pool.getBookedNum() + 1 + ); + if (updated == 0) { + // 并发冲突,回滚事务 + logger.warn("并发预约导致号源已满,poolId={}, expectedBookedNum={}", pool.getId(), pool.getBookedNum()); + throw new BusinessException("该号源已被抢完,请重新选择"); + } + + // 8. 返回订单号,后续前端会调支付接口,支付成功后再将订单状态改为已支付 + return orderMain.getOrderNo(); } - /* -------------------------------------------------------------- - * 医嘱查询相关(包括对历史 null 的兜底填充) - * -------------------------------------------------------------- */ - @Override - public List listOrderDetails(Long orderMainId) { - List details = orderDetailMapper.selectByOrderMainId(orderMainId); - if (CollectionUtils.isEmpty(details)) { - return details; - } + // 其它业务方法保持不变 ... - // 对于 totalAmountUnit 为 null 的记录,尝试补齐(兼容历史数据) - List needFillIds = details.stream() - .filter(d -> !StringUtils.hasText(d.getTotalAmountUnit()) && d.getCatalogItemId() != null) - .map(OrderDetail::getId) - .collect(Collectors.toList()); - - if (!needFillIds.isEmpty()) { - // 批量查询对应的目录项 - List catalogItems = catalogItemMapper.selectByIds( - details.stream() - .filter(d -> needFillIds.contains(d.getId())) - .map(OrderDetail::getCatalogItemId) - .distinct() - .collect(Collectors.toList()) - ); - // 构造映射 - java.util.Map catalogIdToUnit = catalogItems.stream() - .collect(Collectors.toMap(CatalogItem::getId, CatalogItem::getUnit)); - - // 填充并同步回库(一次性更新,避免循环多次写库) - List toUpdate = details.stream() - .filter(d -> needFillIds.contains(d.getId())) - .peek(d -> { - String unit = catalogIdToUnit.get(d.getCatalogItemId()); - if (unit != null) { - d.setTotalAmountUnit(unit); - } - }) - .collect(Collectors.toList()); - - // 批量更新(若对应的 mapper 未提供 batch 方法,则逐条更新) - toUpdate.forEach(d -> orderDetailMapper.updateByPrimaryKey(d)); - } - - return details; + /** + * 生成唯一订单号(简化实现,仅示例) + */ + private String generateOrderNo() { + return "ORD" + System.currentTimeMillis(); } - - // 其余业务方法保持不变... }