Fix: 门诊预约挂号→签到→退号 slot/pool 状态流转对齐需求

- 枚举重排: SlotStatus LOCKED=4→2, CANCELLED=2→4,匹配需求编号
  - 预约: lockSlotForBooking 写入 LOCKED(2) 替代 BOOKED(1),pool locked_num+1 原子递增
  - 签到: LOCKED(2)→BOOKED(1) 替代 CHECKED_IN(3),加前置状态校验
  - 退号: 加 BOOKED(1) 前置校验
  - 池计数: refreshPoolStats booked_num=COUNT(1), locked_num=COUNT(2)
  - SQL 状态值全部由 SlotStatus 枚举传入,消除硬编码
  - 查询/显示: 加 locked 筛选分支,BOOKED→已取号, LOCKED→已锁定
  - 前端常量同步,签到列表查询 book→locked
This commit is contained in:
wangjian963
2026-05-19 12:12:16 +08:00
parent a91ee66368
commit cbad13bddc
12 changed files with 207 additions and 131 deletions

View File

@@ -10,10 +10,11 @@ import org.springframework.stereotype.Repository;
public interface SchedulePoolMapper extends BaseMapper<SchedulePool> {
/**
* 按号源池实时重算统计值,避免并发场景下计数漂移
* 按号源池实时重算统计值。
*
* 说明available_num 在当前项目中可能为数据库生成列,因此这里仅维护
* booked_num / locked_num剩余号由数据库或查询逻辑计算。
* @param poolId 号源池ID
* @param bookedStatus 已约状态值,由 SlotStatus.BOOKED.getValue() 传入
* @param lockedStatus 锁定状态值,由 SlotStatus.LOCKED.getValue() 传入
*/
@Update("""
UPDATE adm_schedule_pool p
@@ -23,20 +24,22 @@ public interface SchedulePoolMapper extends BaseMapper<SchedulePool> {
FROM adm_schedule_slot s
WHERE s.pool_id = p.id
AND s.delete_flag = '0'
AND s.status = 1
AND s.status = #{bookedStatus}
), 0),
locked_num = COALESCE((
SELECT COUNT(1)
FROM adm_schedule_slot s
WHERE s.pool_id = p.id
AND s.delete_flag = '0'
AND s.status = 3
AND s.status = #{lockedStatus}
), 0),
update_time = now()
WHERE p.id = #{poolId}
AND p.delete_flag = '0'
""")
int refreshPoolStats(@Param("poolId") Long poolId);
int refreshPoolStats(@Param("poolId") Long poolId,
@Param("bookedStatus") Integer bookedStatus,
@Param("lockedStatus") Integer lockedStatus);
/**
* 签到时更新号源池统计:锁定数-1已预约数+1

View File

@@ -22,9 +22,12 @@ public interface ScheduleSlotMapper extends BaseMapper<ScheduleSlot> {
TicketSlotDTO selectTicketSlotById(@Param("id") Long id);
/**
* 原子抢占槽位:仅当当前状态=0(可用)时,更新为1(已预约)
* 原子抢占槽位:仅当当前状态=0(待约)时,更新为目标锁定状态
*
* @param slotId 槽位ID
* @param lockedStatus 锁定状态值,由 SlotStatus.LOCKED.getValue() 传入
*/
int lockSlotForBooking(@Param("slotId") Long slotId);
int lockSlotForBooking(@Param("slotId") Long slotId, @Param("lockedStatus") Integer lockedStatus);
/**
* 按主键更新槽位状态。
@@ -34,12 +37,16 @@ public interface ScheduleSlotMapper extends BaseMapper<ScheduleSlot> {
/**
* 更新槽位状态并记录签到时间
*
* @param slotId 槽位ID
* @param status 状态
* @param checkInTime 签到时间
* @param slotId 槽位ID
* @param status 目标状态,由 SlotStatus.BOOKED.getValue() 传入
* @param checkInTime 签到时间
* @param requiredStatus 前置状态,由 SlotStatus.LOCKED.getValue() 传入
* @return 结果
*/
int updateSlotStatusAndCheckInTime(@Param("slotId") Long slotId, @Param("status") Integer status, @Param("checkInTime") Date checkInTime);
int updateSlotStatusAndCheckInTime(@Param("slotId") Long slotId,
@Param("status") Integer status,
@Param("checkInTime") Date checkInTime,
@Param("requiredStatus") Integer requiredStatus);
/**
* 根据槽位ID查询所属号源池ID。

View File

@@ -1,10 +1,12 @@
package com.openhis.clinical.service.impl;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.openhis.appointmentmanage.domain.AppointmentConfig;
import com.openhis.appointmentmanage.service.IAppointmentConfigService;
import com.openhis.appointmentmanage.domain.TicketSlotDTO;
import com.openhis.appointmentmanage.domain.SchedulePool;
import com.openhis.appointmentmanage.domain.ScheduleSlot;
import com.openhis.appointmentmanage.mapper.SchedulePoolMapper;
import com.openhis.appointmentmanage.mapper.ScheduleSlotMapper;
@@ -13,7 +15,7 @@ import com.openhis.clinical.domain.Ticket;
import com.openhis.clinical.mapper.TicketMapper;
import com.openhis.clinical.service.IOrderService;
import com.openhis.clinical.service.ITicketService;
import com.openhis.common.constant.CommonConstants.SlotStatus;
import com.openhis.common.enums.SlotStatus;
import com.openhis.common.enums.OrderStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -177,7 +179,7 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
logger.error("安全拦截号源底库核对失败slotId: {}", slotId);
throw new RuntimeException("号源数据不存在");
}
if (slot.getSlotStatus() != null && !SlotStatus.AVAILABLE.equals(slot.getSlotStatus())) {
if (slot.getSlotStatus() != null && SlotStatus.getByValue(slot.getSlotStatus()) != SlotStatus.AVAILABLE) {
throw new RuntimeException("手慢了!该号源已刚刚被他人抢占");
}
if (Boolean.TRUE.equals(slot.getIsStopped())) {
@@ -205,7 +207,7 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
}
// 原子抢占:避免并发下同一槽位被重复预约
int lockRows = scheduleSlotMapper.lockSlotForBooking(slotId);
int lockRows = scheduleSlotMapper.lockSlotForBooking(slotId, SlotStatus.LOCKED.getValue());
if (lockRows <= 0) {
throw new RuntimeException("手慢了!该号源已刚刚被他人抢占");
}
@@ -260,7 +262,15 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
throw new RuntimeException("预约成功但号源回填订单失败,请重试");
}
refreshPoolStatsBySlotId(slotId);
// 6. 预约成功后 locked_num+1原子递增替代全量 recount避免并发计数漂移
Long poolId = scheduleSlotMapper.selectPoolIdBySlotId(slotId);
if (poolId != null) {
schedulePoolMapper.update(null,
new LambdaUpdateWrapper<SchedulePool>()
.setSql("locked_num = locked_num + 1, version = version + 1")
.set(SchedulePool::getUpdateTime, new Date())
.eq(SchedulePool::getId, poolId));
}
return 1;
}
@@ -277,7 +287,8 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
if (slot == null) {
throw new RuntimeException("号源槽位不存在");
}
if (slot.getSlotStatus() == null || !SlotStatus.BOOKED.equals(slot.getSlotStatus())) {
// 只有锁定态(2)的号源可以取消预约
if (slot.getSlotStatus() == null || SlotStatus.getByValue(slot.getSlotStatus()) != SlotStatus.LOCKED) {
throw new RuntimeException("号源不可取消预约");
}
@@ -292,7 +303,7 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
orderService.cancelAppointmentOrder(order.getId(), "患者取消预约");
}
int updated = scheduleSlotMapper.updateSlotStatus(slotId, SlotStatus.AVAILABLE);
int updated = scheduleSlotMapper.updateSlotStatus(slotId, SlotStatus.AVAILABLE.getValue());
if (updated > 0) {
refreshPoolStatsBySlotId(slotId);
}
@@ -318,11 +329,14 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
orderService.updateOrderStatusById(latestOrder.getId(), OrderStatus.ACTIVE.getValue());
orderMapper.updatePayStatus(latestOrder.getId(), 1, new Date());
// 2. 查询号源槽位信息
// 2. 只有锁定态(2)的号源才能签到,签到时 2→1(LOCKED→BOOKED)
ScheduleSlot slot = scheduleSlotMapper.selectById(slotId);
if (slot == null || !SlotStatus.LOCKED.getValue().equals(slot.getStatus())) {
throw new RuntimeException("号源状态异常,无法签到");
}
// 3. 更新号源槽位状态为已签到,记录签到时间
scheduleSlotMapper.updateSlotStatusAndCheckInTime(slotId, SlotStatus.CHECKED_IN, new Date());
// 3. 更新号源槽位状态 2→1LOCKED→BOOKED已预约=已签到)
scheduleSlotMapper.updateSlotStatusAndCheckInTime(slotId, SlotStatus.BOOKED.getValue(), new Date(), SlotStatus.LOCKED.getValue());
// 4. 更新号源池统计:锁定数-1已预约数+1
if (slot != null && slot.getPoolId() != null) {
@@ -351,7 +365,7 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
orderService.cancelAppointmentOrder(order.getId(), "医生停诊");
}
int updated = scheduleSlotMapper.updateSlotStatus(slotId, SlotStatus.CANCELLED);
int updated = scheduleSlotMapper.updateSlotStatus(slotId, SlotStatus.CANCELLED.getValue());
if (updated > 0) {
refreshPoolStatsBySlotId(slotId);
}
@@ -364,7 +378,7 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
private void refreshPoolStatsBySlotId(Long slotId) {
Long poolId = scheduleSlotMapper.selectPoolIdBySlotId(slotId);
if (poolId != null) {
schedulePoolMapper.refreshPoolStats(poolId);
schedulePoolMapper.refreshPoolStats(poolId, SlotStatus.BOOKED.getValue(), SlotStatus.LOCKED.getValue());
}
}