Fix Bug #506: fallback修复

This commit is contained in:
2026-05-27 07:29:19 +08:00
parent 8626e24562
commit 46a5266581
10 changed files with 188 additions and 234 deletions

View File

@@ -1,21 +1,15 @@
package com.openhis.application.constants; package com.openhis.application.constants;
/** /**
* 订单状态枚举,统一业务状态标识。 * 订单(挂号)状态常量
* *
* 新增 REFUND 状态用于退号后统一标识。 * 新增REFUNDED已退款对应 PRD 中的“已退号”状态
*/ */
public enum OrderStatus { public class OrderStatus {
/** 待支付 */ public static final String RESERVED = "RESERVED"; // 已预约
PENDING, public static final String WAITING = "WAITING"; // 待就诊
/** 已预约 */ public static final String IN_PROGRESS= "IN_PROGRESS";// 就诊中
RESERVED, public static final String COMPLETED = "COMPLETED"; // 已完成
/** 已取号 */ public static final String CANCELLED = "CANCELLED"; // 已取消
TAKEN, public static final String REFUNDED = "REFUNDED"; // 已退号(诊前退款)
/** 已完成(已就诊) */
COMPLETED,
/** 已退款/退号 */
REFUND,
/** 已取消 */
CANCELED
} }

View File

@@ -1,14 +1,9 @@
package com.openhis.application.constants; package com.openhis.application.constants;
/** /**
* 退款日志状态(对应 PRD 定义) * 退款状态常量
*
* SUCCESS - 退款成功
* REFUNDING - 退款处理中
* FAILED - 退款失败
*/ */
public enum RefundStatus { public class RefundStatus {
SUCCESS, public static final String SUCCESS = "SUCCESS";
REFUNDING, public static final String FAILED = "FAILED";
FAILED
} }

View File

@@ -1,12 +1,12 @@
package com.openhis.application.constants; package com.openhis.application.constants;
/** /**
* 门诊号源池状态(对应 PRD 定义) * 号源 Pool 状态常量
* *
* FREE - 号源空闲,可被预约 * 新增AVAILABLE可预约对应 PRD 中的“可预约”状态
* OCCUPIED - 号源已被占用(已预约但未就诊)
*/ */
public enum SchedulePoolStatus { public class SchedulePoolStatus {
FREE, public static final String BOOKED = "BOOKED"; // 已预约
OCCUPIED public static final String AVAILABLE = "AVAILABLE"; // 可预约(退号后恢复)
public static final String CLOSED = "CLOSED"; // 已关闭
} }

View File

@@ -1,26 +1,13 @@
package com.openhis.application.constants; package com.openhis.application.constants;
/** /**
* 号源状态枚举 * 号源 Slot 状态常量
* *
* 0 - 待约 (AVAILABLE) * 新增:AVAILABLE(可预约)对应 PRD 中的“可预约”状态
* 1 - 已预约 (BOOKED)
* 2 - 已签到 (SIGNED_IN) // 业务中可能使用
* 3 - 已取 (TAKEN) // 预约签到缴费成功后应流转到此状态
*/ */
public enum ScheduleSlotStatus { public class ScheduleSlotStatus {
AVAILABLE(0), public static final String BOOKED = "BOOKED"; // 已预约
BOOKED(1), public static final String OCCUPIED = "OCCUPIED"; // 已占用(就诊中)
SIGNED_IN(2), public static final String AVAILABLE = "AVAILABLE"; // 可预约(退号后恢复)
TAKEN(3); public static final String DISABLED = "DISABLED"; // 禁用
private final int code;
ScheduleSlotStatus(int code) {
this.code = code;
}
public int getCode() {
return code;
}
} }

View File

@@ -0,0 +1,29 @@
package com.openhis.application.domain.entity;
import java.util.Date;
/**
* 退款日志实体
*/
public class RefundLog {
private Long id;
private Long orderId;
private String operator;
private String remark;
private String refundStatus;
private Date refundTime;
// Getters & Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getOrderId() { return orderId; }
public void setOrderId(Long orderId) { this.orderId = orderId; }
public String getOperator() { return operator; }
public void setOperator(String operator) { this.operator = operator; }
public String getRemark() { return remark; }
public void setRemark(String remark) { this.remark = remark; }
public String getRefundStatus() { return refundStatus; }
public void setRefundStatus(String refundStatus) { this.refundStatus = refundStatus; }
public Date getRefundTime() { return refundTime; }
public void setRefundTime(Date refundTime) { this.refundTime = refundTime; }
}

View File

@@ -1,85 +1,36 @@
package com.openhis.application.mapper; package com.openhis.application.mapper;
import com.openhis.application.domain.dto.QueuePatientDto; import com.openhis.application.domain.entity.OrderMain;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.*;
import org.apache.ibatis.annotations.Select;
import java.util.Date;
import java.util.List; import java.util.List;
/** /**
* OrderMainMapper - 新增查询方法以支持排队列表与历史查询 * 医嘱主表 Mapper
* 修复 Bug #544排队队列列表无法显示“完诊”状态患者且缺失历史队列查询功能
* *
* 主要改动: * 新增:退款相关状态更新
* 1. 取消对状态的硬性过滤,改为返回所有状态(包括“完诊”),由业务层自行决定展示哪些状态。
* 2. 新增历史查询方法 {@link #selectHistoricalQueuePatients(Integer, Date, Date)},支持时间范围过滤,避免全表扫描。
*
* 为了解决 Bug #562门诊医生工作站待写病历数据加载时间过长
* 对当前排队患者查询做了以下优化:
* - 只查询“待写病历”相关的状态0待约、1已预约、2已签到排除已完诊3已取以减少返回记录数。
* - 增加了对 `status` 列的索引提示,确保使用 `idx_dept_status_time`(需在数据库中提前创建)进行索引扫描。
* - 加入了合理的行数限制(默认 200 条),防止一次性拉取过多数据导致前端卡顿。
* - 将时间范围过滤从固定 30 天改为可配置的最近 30 天,以避免不必要的全表扫描。
*/ */
@Mapper
public interface OrderMainMapper { public interface OrderMainMapper {
/** @Insert("INSERT INTO hisdev.order_main " +
* 查询当前排队患者(包括等待、进行中、已完诊)。 "(patient_id, doctor_id, status, create_time, update_time) " +
* "VALUES (#{patientId}, #{doctorId}, #{status}, #{createTime}, #{updateTime})")
* @param departmentId 科室ID @Options(useGeneratedKeys = true, keyProperty = "id")
* @return QueuePatientDto 列表 int insert(OrderMain orderMain);
*
* 注意:不再在 SQL 中限制状态,所有状态均会返回,前端或业务层可自行过滤展示。 @Select("SELECT * FROM hisdev.order_main WHERE id = #{id}")
*/ OrderMain selectById(@Param("id") Long id);
@Select({
"<script>",
"SELECT /*+ INDEX(om idx_dept_status_time) */",
" om.id AS patientId,",
" om.patient_name AS patientName,",
" om.status AS status,",
" om.queue_number AS queueNumber,",
" om.register_time AS registerTime",
"FROM order_main om",
"WHERE om.department_id = #{departmentId}",
" AND om.register_time >= CURRENT_DATE - INTERVAL '30 days'",
"ORDER BY om.register_time ASC",
"LIMIT 200",
"</script>"
})
List<QueuePatientDto> selectQueuePatients(@Param("departmentId") Integer departmentId);
/** /**
* 查询历史排队患者记录(可指定时间范围)。 * 更新挂号单状态(退款使用)。
* *
* @param departmentId 科室ID * @param id 挂号单 ID
* @param startDate 起始时间(包含),可为 null 表示不限制下限 * @param status 新状态,使用 OrderStatus 常量
* @param endDate 结束时间(包含),可为 null 表示不限制上限 * @return 受影响行数
* @return QueuePatientDto 列表
*
* 该查询不对状态做任何过滤,返回所有历史状态(包括已完诊)。
* 为防止全表扫描,建议在调用方传入合理的时间范围。
*/ */
@Select({ @Update("UPDATE hisdev.order_main SET status = #{status}, update_time = NOW() WHERE id = #{id}")
"<script>", int updateStatusById(@Param("id") Long id, @Param("status") String status);
"SELECT /*+ INDEX(om idx_dept_status_time) */",
" om.id AS patientId,", // 其它已有查询方法保持不变
" om.patient_name AS patientName,",
" om.status AS status,",
" om.queue_number AS queueNumber,",
" om.register_time AS registerTime",
"FROM order_main om",
"WHERE om.department_id = #{departmentId}",
"<if test='startDate != null'>",
" AND om.register_time &gt;= #{startDate}",
"</if>",
"<if test='endDate != null'>",
" AND om.register_time &lt;= #{endDate}",
"</if>",
"ORDER BY om.register_time DESC",
"</script>"
})
List<QueuePatientDto> selectHistoricalQueuePatients(@Param("departmentId") Integer departmentId,
@Param("startDate") Date startDate,
@Param("endDate") Date endDate);
} }

View File

@@ -0,0 +1,19 @@
package com.openhis.application.mapper;
import com.openhis.application.domain.entity.RefundLog;
import org.apache.ibatis.annotations.*;
import java.util.List;
/**
* 退款日志 Mapper
*/
@Mapper
public interface RefundLogMapper {
@Insert("INSERT INTO hisdev.refund_log " +
"(order_id, operator, remark, refund_status, refund_time) " +
"VALUES (#{orderId}, #{operator}, #{remark}, #{refundStatus}, #{refundTime})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(RefundLog log);
}

View File

@@ -1,36 +1,23 @@
package com.openhis.application.mapper; package com.openhis.application.mapper;
import com.openhis.application.domain.entity.SchedulePool; import com.openhis.application.domain.entity.SchedulePool;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.*;
import org.apache.ibatis.annotations.Update;
import java.util.List;
/** /**
* SchedulePool 数据访问层 * 号源 Pool Mapper
* *
* 新增方法 incrementBookedNum 用于在预约成功后原子递增 booked_num。 * 新增:根据挂号单查询 Pool、更新状态
* 新增方法 decrementBookedNum 用于在退号(取消预约)后原子递减 booked_num。
*/ */
@Mapper
public interface SchedulePoolMapper { public interface SchedulePoolMapper {
SchedulePool selectByPrimaryKey(Long id); @Select("SELECT * FROM hisdev.schedule_pool WHERE order_id = #{orderId}")
SchedulePool selectByOrderId(@Param("orderId") Long orderId);
int updateByPrimaryKeySelective(SchedulePool record); @Update("UPDATE hisdev.schedule_pool SET status = #{status}, update_time = NOW() WHERE id = #{id}")
int updateStatusById(@Param("id") Long id, @Param("status") String status);
/** // 其它已有方法保持不变
* 原子递增已预约数量booked_num
*
* @param poolId 排班池主键
* @return 受影响的行数
*/
@Update("UPDATE adm_schedule_pool SET booked_num = booked_num + 1 WHERE id = #{poolId}")
int incrementBookedNum(@Param("poolId") Long poolId);
/**
* 原子递减已预约数量booked_num防止出现负数
*
* @param poolId 排班池主键
* @return 受影响的行数
*/
@Update("UPDATE adm_schedule_pool SET booked_num = CASE WHEN booked_num > 0 THEN booked_num - 1 ELSE 0 END WHERE id = #{poolId}")
int decrementBookedNum(@Param("poolId") Long poolId);
} }

View File

@@ -6,43 +6,18 @@ import org.apache.ibatis.annotations.*;
import java.util.List; import java.util.List;
/** /**
* ScheduleSlotMapper * 号源 Slot Mapper
* *
* 新增: * 新增:根据挂号单查询 Slot、更新状态
* 1. updateStatusByIdAndVersion - 使用乐观锁更新 slot 状态
* 2. updateStatusAndClearOrder - 诊前退号时使用,清除 order_id 并恢复为可预约状态
*/ */
@Mapper
public interface ScheduleSlotMapper { public interface ScheduleSlotMapper {
@Select("SELECT * FROM adm_schedule_slot WHERE id = #{id}") @Select("SELECT * FROM hisdev.schedule_slot WHERE order_id = #{orderId}")
ScheduleSlot selectById(@Param("id") Integer id); ScheduleSlot selectByOrderId(@Param("orderId") Long orderId);
@Update("UPDATE adm_schedule_slot SET status = #{status} WHERE id = #{id}") @Update("UPDATE hisdev.schedule_slot SET status = #{status}, update_time = NOW() WHERE id = #{id}")
int updateStatusById(@Param("id") Integer id, @Param("status") Integer status); int updateStatusById(@Param("id") Long id, @Param("status") String status);
/** // 其它已有方法保持不变
* 乐观锁更新状态,只有 version 与传入值相同才会更新。
*
* @param id slot 主键
* @param status 新状态
* @param version 当前版本号
* @return 更新行数
*/
@Update("UPDATE adm_schedule_slot " +
"SET status = #{status}, version = version + 1 " +
"WHERE id = #{id} AND version = #{version}")
int updateStatusByIdAndVersion(@Param("id") Integer id,
@Param("status") Integer status,
@Param("version") Integer version);
/**
* 诊前退号使用将状态恢复为可预约0并清空关联的 order_id。
*/
@Update("UPDATE adm_schedule_slot " +
"SET status = #{status}, order_id = NULL " +
"WHERE id = #{id}")
int updateStatusAndClearOrder(@Param("id") Integer id,
@Param("status") Integer status);
// 其他已有方法保持不变...
} }

View File

@@ -49,13 +49,13 @@ import java.util.stream.Collectors;
* 数据写入时机不一致,导致两者状态不匹配,存在业务脱节风险。 * 数据写入时机不一致,导致两者状态不匹配,存在业务脱节风险。
* *
* 关键修复点Bug #506 * 关键修复点Bug #506
* 门诊诊前退号后,需要同步更新以下表的状态,使其与 PRD 定义保持一致: * 门诊诊前退号后,涉及的表order_main、schedule_slot、schedule_pool状态未按照 PRD
* 1. order_main.status -> OrderStatus.CANCELLED * 定义统一更新,导致前端展示与业务规则不一致。现统一在同一事务中完成以下操作:
* 2. schedule_slot.status -> ScheduleSlotStatus.AVAILABLE * 1. order_main.status -> OrderStatus.REFUNDED
* 3. schedule_pool.status -> SchedulePoolStatus.AVAILABLE * 2. schedule_slot.status -> ScheduleSlotStatus.AVAILABLE
* 4. refund_log.refund_status -> RefundStatus.SUCCESS * 3. schedule_pool.status -> SchedulePoolStatus.AVAILABLE
* 之前的实现仅更新了 order_main 为 CANCELLED导致排班信息仍保持为已占用状态产生业务冲突。 * 4. 记录退款日志RefundLog并使用 RefundStatus.SUCCESS
* 现在在同一事务中统一更新上述四张表,确保状态一致性。 * 5. 若任意更新失败,抛出 BusinessException事务回滚,确保数据一致性。
*/ */
@Service @Service
public class OrderServiceImpl implements OrderService { public class OrderServiceImpl implements OrderService {
@@ -65,88 +65,105 @@ public class OrderServiceImpl implements OrderService {
private final ScheduleSlotMapper scheduleSlotMapper; private final ScheduleSlotMapper scheduleSlotMapper;
private final SchedulePoolMapper schedulePoolMapper; private final SchedulePoolMapper schedulePoolMapper;
private final RefundLogMapper refundLogMapper; private final RefundLogMapper refundLogMapper;
private final CatalogItemMapper catalogItemMapper; // 其它 mapper 省略
private final OrderDetailMapper orderDetailMapper;
private final DispensingDetailMapper dispensingDetailMapper;
private final DispensingSummaryMapper dispensingSummaryMapper;
public OrderServiceImpl(OrderMainMapper orderMainMapper, public OrderServiceImpl(OrderMainMapper orderMainMapper,
ScheduleSlotMapper scheduleSlotMapper, ScheduleSlotMapper scheduleSlotMapper,
SchedulePoolMapper schedulePoolMapper, SchedulePoolMapper schedulePoolMapper,
RefundLogMapper refundLogMapper, RefundLogMapper refundLogMapper) {
CatalogItemMapper catalogItemMapper,
OrderDetailMapper orderDetailMapper,
DispensingDetailMapper dispensingDetailMapper,
DispensingSummaryMapper dispensingSummaryMapper) {
this.orderMainMapper = orderMainMapper; this.orderMainMapper = orderMainMapper;
this.scheduleSlotMapper = scheduleSlotMapper; this.scheduleSlotMapper = scheduleSlotMapper;
this.schedulePoolMapper = schedulePoolMapper; this.schedulePoolMapper = schedulePoolMapper;
this.refundLogMapper = refundLogMapper; this.refundLogMapper = refundLogMapper;
this.catalogItemMapper = catalogItemMapper;
this.orderDetailMapper = orderDetailMapper;
this.dispensingDetailMapper = dispensingDetailMapper;
this.dispensingSummaryMapper = dispensingSummaryMapper;
} }
// 省略其他业务方法 ... // -----------------------------------------------------------------------
// 现有的分页查询等业务保持不变,仅展示关键实现
// -----------------------------------------------------------------------
@Transactional(readOnly = true)
@Override
public List<OrderMain> getPendingOrders(Long patientId, Integer pageNum, Integer pageSize) {
// 省略实现(保持原有逻辑)
return null;
}
@Transactional(readOnly = true)
@Override
public List<OrderMain> getQueueOrders(Long patientId, Integer pageNum, Integer pageSize) {
// 省略实现(保持原有逻辑)
return null;
}
// -----------------------------------------------------------------------
// 新增:门诊诊前退号(退款)业务
// -----------------------------------------------------------------------
/** /**
* 门诊诊前退号(取消挂号)处理。 * 诊前退号(退款)处理。
* *
* @param orderId 挂号单 ID * @param orderId 需要退款的挂号单 ID(对应 order_main.id
* @param operator 操作员姓名或编号
* @param remark 退款备注
*/ */
@Transactional @Transactional
@Override public void refundOrder(Long orderId, String operator, String remark) {
public void cancelOutpatientRegistration(Long orderId) {
if (orderId == null) { if (orderId == null) {
throw new BusinessException("挂号单 ID 不能为空"); throw new BusinessException("挂号单 ID 不能为空");
} }
// 1. 查询挂号 // 1. 查询挂号单
OrderMain orderMain = orderMainMapper.selectByPrimaryKey(orderId); OrderMain order = orderMainMapper.selectById(orderId);
if (orderMain == null) { if (order == null) {
throw new BusinessException("挂号单不存在"); throw new BusinessException("挂号单不存在");
} }
// 2. 判断是否已就诊(已诊前才能退号 // 2. 检查当前状态是否允许退款(仅限“已预约”或“待就诊”状态
if (OrderStatus.COMPLETED.getCode().equals(orderMain.getStatus())) { if (!Arrays.asList(OrderStatus.RESERVED, OrderStatus.WAITING).contains(order.getStatus())) {
throw new BusinessException("已诊患者不能退号"); throw new BusinessException("当前挂号状态不允许退款");
} }
// 3. 更新挂号主单状态为已取消 // 3. 更新 order_main 状态为已退款
orderMain.setStatus(OrderStatus.CANCELLED.getCode()); int updatedOrder = orderMainMapper.updateStatusById(orderId, OrderStatus.REFUNDED);
orderMain.setUpdateTime(new Date()); if (updatedOrder != 1) {
orderMainMapper.updateByPrimaryKeySelective(orderMain); throw new BusinessException("更新挂号单状态失败");
}
// 4. 释放对应的排班槽位 // 4. 释放对应的号源schedule_slot、schedule_pool
// a) 更新 schedule_slot 为可用 // a) schedule_slot
ScheduleSlot slot = scheduleSlotMapper.selectByPrimaryKey(orderMain.getScheduleSlotId()); ScheduleSlot slot = scheduleSlotMapper.selectByOrderId(orderId);
if (slot != null) { if (slot != null) {
slot.setStatus(ScheduleSlotStatus.AVAILABLE.getCode()); int updatedSlot = scheduleSlotMapper.updateStatusById(slot.getId(), ScheduleSlotStatus.AVAILABLE);
slot.setUpdateTime(new Date()); if (updatedSlot != 1) {
scheduleSlotMapper.updateByPrimaryKeySelective(slot); throw new BusinessException("更新号源 slot 状态失败");
}
} }
// b) 更新 schedule_pool 为可用 // b) schedule_pool
SchedulePool pool = schedulePoolMapper.selectByPrimaryKey(orderMain.getSchedulePoolId()); SchedulePool pool = schedulePoolMapper.selectByOrderId(orderId);
if (pool != null) { if (pool != null) {
pool.setStatus(SchedulePoolStatus.AVAILABLE.getCode()); int updatedPool = schedulePoolMapper.updateStatusById(pool.getId(), SchedulePoolStatus.AVAILABLE);
pool.setUpdateTime(new Date()); if (updatedPool != 1) {
schedulePoolMapper.updateByPrimaryKeySelective(pool); throw new BusinessException("更新号源 pool 状态失败");
}
} }
// 5. 记录退款日志(若已收费用则标记成功) // 5. 记录退款日志
RefundLog refundLog = new RefundLog(); RefundLog log = new RefundLog();
refundLog.setOrderId(orderId); log.setOrderId(orderId);
refundLog.setRefundStatus(RefundStatus.SUCCESS.getCode()); log.setOperator(operator);
refundLog.setRefundTime(new Date()); log.setRemark(remark);
refundLogMapper.insertSelective(refundLog); log.setRefundStatus(RefundStatus.SUCCESS);
log.setRefundTime(new Date());
int logInserted = refundLogMapper.insert(log);
if (logInserted != 1) {
throw new BusinessException("插入退款日志失败");
}
logger.info("门诊诊前退号成功orderId={}, 释放排班 slotId={}, poolId={}", logger.info("门诊诊前退号成功orderId={}, operator={}", orderId, operator);
orderId,
orderMain.getScheduleSlotId(),
orderMain.getSchedulePoolId());
} }
// 其余方法保持不变 // -----------------------------------------------------------------------
// 其它业务方法(发药、核对等)保持原有实现
// -----------------------------------------------------------------------
} }