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:
@@ -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
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -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→1(LOCKED→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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,17 @@
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.openhis.appointmentmanage.mapper.ScheduleSlotMapper">
|
||||
|
||||
<!-- 统一状态值(兼容数字/英文字符串存储),输出 Integer,避免 resultType 映射 NumberFormatException -->
|
||||
<!--
|
||||
统一状态值映射: DB 数值 → 规范化输出
|
||||
0=待约 1=已约(签到后) 2=锁定(预约后) 3=已签到 4=已停诊 5=已退号
|
||||
-->
|
||||
<sql id="slotStatusNormExpr">
|
||||
CASE
|
||||
WHEN LOWER(CONCAT('', s.status)) IN ('0', 'unbooked', 'available') THEN 0
|
||||
WHEN LOWER(CONCAT('', s.status)) IN ('1', 'booked') THEN 1
|
||||
WHEN LOWER(CONCAT('', s.status)) IN ('2', 'cancelled', 'canceled', 'stopped') THEN 2
|
||||
WHEN LOWER(CONCAT('', s.status)) IN ('2', 'locked') THEN 2
|
||||
WHEN LOWER(CONCAT('', s.status)) IN ('3', 'checked', 'checked_in', 'checkin') THEN 3
|
||||
WHEN LOWER(CONCAT('', s.status)) IN ('4', 'locked') THEN 4
|
||||
WHEN LOWER(CONCAT('', s.status)) IN ('4', 'cancelled', 'canceled', 'stopped') THEN 4
|
||||
WHEN LOWER(CONCAT('', s.status)) IN ('5', 'returned') THEN 5
|
||||
ELSE NULL
|
||||
END
|
||||
@@ -31,9 +34,9 @@
|
||||
CASE
|
||||
WHEN LOWER(CONCAT('', p.status)) IN ('0', 'unbooked', 'available') THEN 0
|
||||
WHEN LOWER(CONCAT('', p.status)) IN ('1', 'booked') THEN 1
|
||||
WHEN LOWER(CONCAT('', p.status)) IN ('2', 'cancelled', 'canceled', 'stopped') THEN 2
|
||||
WHEN LOWER(CONCAT('', p.status)) IN ('2', 'locked') THEN 2
|
||||
WHEN LOWER(CONCAT('', p.status)) IN ('3', 'checked', 'checked_in', 'checkin') THEN 3
|
||||
WHEN LOWER(CONCAT('', p.status)) IN ('4', 'locked') THEN 4
|
||||
WHEN LOWER(CONCAT('', p.status)) IN ('4', 'cancelled', 'canceled', 'stopped') THEN 4
|
||||
WHEN LOWER(CONCAT('', p.status)) IN ('5', 'returned') THEN 5
|
||||
ELSE NULL
|
||||
END
|
||||
@@ -149,10 +152,11 @@
|
||||
s.id = #{id}
|
||||
</select>
|
||||
|
||||
<!-- 预约锁定: 0→#{lockedStatus} (AVAILABLE→LOCKED),由枚举传入 -->
|
||||
<update id="lockSlotForBooking">
|
||||
UPDATE adm_schedule_slot
|
||||
SET
|
||||
status = 1,
|
||||
status = #{lockedStatus},
|
||||
update_time = now()
|
||||
WHERE
|
||||
id = #{slotId}
|
||||
@@ -174,6 +178,7 @@
|
||||
AND delete_flag = '0'
|
||||
</update>
|
||||
|
||||
<!-- 签到: #{requiredStatus}→#{status} (LOCKED→BOOKED),前置条件由枚举传入 -->
|
||||
<update id="updateSlotStatusAndCheckInTime">
|
||||
UPDATE adm_schedule_slot
|
||||
SET
|
||||
@@ -182,6 +187,7 @@
|
||||
update_time = NOW()
|
||||
WHERE
|
||||
id = #{slotId}
|
||||
AND status = #{requiredStatus}
|
||||
AND delete_flag = '0'
|
||||
</update>
|
||||
|
||||
@@ -202,7 +208,7 @@
|
||||
update_time = now()
|
||||
WHERE
|
||||
id = #{slotId}
|
||||
AND status = 1
|
||||
AND status = 2
|
||||
AND delete_flag = '0'
|
||||
</update>
|
||||
|
||||
@@ -299,15 +305,16 @@
|
||||
<if test="query.phone != null and query.phone != ''">
|
||||
AND o.phone LIKE CONCAT('%', #{query.phone}, '%')
|
||||
</if>
|
||||
<!-- 5. 按系统时间过滤(Bug #398 #399 修复:仅未预约受时间过滤,已预约/已取号/已退号不受影响) -->
|
||||
<!-- 5. 时间过滤: 仅待约(0)受时间限制,已锁定(2)/已约(1)/已签到(3)/已退号(5)不受影响 -->
|
||||
AND (
|
||||
(<include refid="slotStatusNormExpr" /> = 0 AND (p.schedule_date > CURRENT_DATE OR (p.schedule_date = CURRENT_DATE AND (CAST(p.schedule_date AS TIMESTAMP) + CAST(s.expect_time AS TIME)) >= NOW())))
|
||||
OR <include refid="slotStatusNormExpr" /> = 1
|
||||
OR <include refid="slotStatusNormExpr" /> = 2
|
||||
OR <include refid="slotStatusNormExpr" /> = 3
|
||||
OR <include refid="slotStatusNormExpr" /> = 5
|
||||
OR <include refid="orderStatusNormExpr" /> = 4
|
||||
)
|
||||
<!-- 6. 状态过滤 -->
|
||||
<!-- 6. 状态筛选: unbooked(0) locked(2) booked(2) checked(1) cancelled(4) returned(5) -->
|
||||
<if test="query.status != null and query.status != '' and query.status != 'all'">
|
||||
<choose>
|
||||
<when test="'unbooked'.equals(query.status) or '未预约'.equals(query.status)">
|
||||
@@ -318,7 +325,15 @@
|
||||
)
|
||||
</when>
|
||||
<when test="'booked'.equals(query.status) or '已预约'.equals(query.status)">
|
||||
AND <include refid="slotStatusNormExpr" /> = 1
|
||||
AND <include refid="slotStatusNormExpr" /> = 2
|
||||
AND <include refid="orderStatusNormExpr" /> = 1
|
||||
AND (
|
||||
d.is_stopped IS NULL
|
||||
OR d.is_stopped = FALSE
|
||||
)
|
||||
</when>
|
||||
<when test="'locked'.equals(query.status) or '已锁定'.equals(query.status)">
|
||||
AND <include refid="slotStatusNormExpr" /> = 2
|
||||
AND <include refid="orderStatusNormExpr" /> = 1
|
||||
AND (
|
||||
d.is_stopped IS NULL
|
||||
@@ -326,13 +341,7 @@
|
||||
)
|
||||
</when>
|
||||
<when test="'checked'.equals(query.status) or '已取号'.equals(query.status)">
|
||||
AND (
|
||||
<include refid="slotStatusNormExpr" /> = 3
|
||||
OR (
|
||||
<include refid="slotStatusNormExpr" /> = 1
|
||||
AND <include refid="orderStatusNormExpr" /> = 2
|
||||
)
|
||||
)
|
||||
AND <include refid="slotStatusNormExpr" /> = 1
|
||||
AND (
|
||||
d.is_stopped IS NULL
|
||||
OR d.is_stopped = FALSE
|
||||
@@ -340,7 +349,7 @@
|
||||
</when>
|
||||
<when test="'cancelled'.equals(query.status) or '已停诊'.equals(query.status) or '已取消'.equals(query.status)">
|
||||
AND (
|
||||
<include refid="slotStatusNormExpr" /> = 2
|
||||
<include refid="slotStatusNormExpr" /> = 4
|
||||
OR d.is_stopped = TRUE
|
||||
)
|
||||
</when>
|
||||
|
||||
Reference in New Issue
Block a user