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

@@ -4,7 +4,7 @@ import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.core.common.core.domain.R;
import com.core.common.utils.SecurityUtils;
import com.openhis.common.constant.CommonConstants;
import com.openhis.common.enums.SlotStatus;
import com.openhis.appointmentmanage.domain.DoctorSchedule;
import com.openhis.appointmentmanage.domain.DoctorScheduleWithDateDto;
import com.openhis.appointmentmanage.domain.SchedulePool;
@@ -502,8 +502,8 @@ public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService {
// 该排班下存在有效患者预约(号源槽:已预约/已锁定/已取号)则禁止删除;已退号、仅可用/已取消槽位不计入
long appointmentCount = scheduleSlotService.count(new QueryWrapper<ScheduleSlot>()
.in("pool_id", poolIds)
.in("status", CommonConstants.SlotStatus.BOOKED, CommonConstants.SlotStatus.LOCKED,
CommonConstants.SlotStatus.CHECKED_IN));
.in("status", SlotStatus.BOOKED.getValue(), SlotStatus.LOCKED.getValue(),
SlotStatus.CHECKED_IN.getValue()));
if (appointmentCount > 0) {
return R.fail("该排班已有患者预约,禁止删除!如需取消请先处理患者退预约或使用'停诊'功能。");
}

View File

@@ -9,7 +9,7 @@ import com.openhis.clinical.domain.Ticket;
import com.openhis.clinical.service.ITicketService;
import com.openhis.web.appointmentmanage.appservice.ITicketAppService;
import com.openhis.web.appointmentmanage.dto.TicketDto;
import com.openhis.common.constant.CommonConstants.SlotStatus;
import com.openhis.common.enums.SlotStatus;
import com.openhis.common.enums.OrderStatus;
import org.springframework.stereotype.Service;
@@ -193,25 +193,24 @@ public class TicketAppServiceImpl implements ITicketAppService {
if (Boolean.TRUE.equals(raw.getIsStopped())) {
dto.setStatus("已停诊");
} else {
Integer slotStatus = raw.getSlotStatus();
if (slotStatus != null) {
if (SlotStatus.CHECKED_IN.equals(slotStatus)) {
dto.setStatus("已取号");
} else if (SlotStatus.BOOKED.equals(slotStatus)) {
// order_main.status: 0=患者取消(已退号) 2=系统取消 其余=已预约
SlotStatus status = SlotStatus.getByValue(raw.getSlotStatus());
if (status != null) {
if (status == SlotStatus.LOCKED) {
if (OrderStatus.PATIENT_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("已退号");
} else if (OrderStatus.SYSTEM_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("系统取消");
} else {
dto.setStatus("预约");
dto.setStatus("锁定");
}
} else if (SlotStatus.RETURNED.equals(slotStatus)) {
dto.setStatus("已退号");
} else if (SlotStatus.CANCELLED.equals(slotStatus)) {
} else if (status == SlotStatus.BOOKED) {
if (OrderStatus.PATIENT_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("已退号");
} else {
dto.setStatus("已取号");
}
} else if (status == SlotStatus.CANCELLED) {
dto.setStatus("已停诊");
} else if (SlotStatus.LOCKED.equals(slotStatus)) {
dto.setStatus("锁定");
} else if (status == SlotStatus.RETURNED) {
dto.setStatus("退号");
} else {
dto.setStatus("未预约");
}
@@ -237,6 +236,10 @@ public class TicketAppServiceImpl implements ITicketAppService {
/**
* 统一状态入参,避免前端状态值大小写/中文/数字差异导致 SQL 条件失效后回全量数据
*/
/**
* 规范前端传入的状态查询参数,映射到 SQL 的 slotStatusNormExpr 值。
* 数值映射: 0=待约 1=已约(签到后) 2=锁定(预约后) 3=已签到 4=已停诊 5=已退号
*/
private void normalizeQueryStatus(com.openhis.appointmentmanage.dto.TicketQueryDTO query) {
String rawStatus = query.getStatus();
if (rawStatus == null) {
@@ -263,28 +266,31 @@ public class TicketAppServiceImpl implements ITicketAppService {
case "已预约":
query.setStatus("booked");
break;
case "locked":
case "2":
case "已锁定":
query.setStatus("locked");
break;
case "checked":
case "checkin":
case "checkedin":
case "2":
case "3":
case "已取号":
query.setStatus("checked");
break;
case "cancelled":
case "canceled":
case "3":
case "4":
case "已停诊":
case "已取消":
query.setStatus("cancelled");
break;
case "returned":
case "4":
case "5":
case "已退号":
query.setStatus("returned");
break;
default:
// 设置为 impossible 值,配合 mapper 的 otherwise 分支直接返回空
query.setStatus("__invalid__");
break;
}
@@ -367,26 +373,25 @@ public class TicketAppServiceImpl implements ITicketAppService {
if (Boolean.TRUE.equals(raw.getIsStopped())) {
dto.setStatus("已停诊");
} else {
// 第二关:看独立的细分槽位状态 (0: 可用, 1: 已预约, 2: 已取消...)
Integer slotStatus = raw.getSlotStatus();
if (slotStatus != null) {
if (SlotStatus.CHECKED_IN.equals(slotStatus)) {
dto.setStatus("已取号");
} else if (SlotStatus.BOOKED.equals(slotStatus)) {
// order_main.status: 0=患者取消(已退号) 2=系统取消 其余=已预约
// 第二关:看独立的细分槽位状态 (0: 可用, 1: 已预约, 2: 已锁定...)
SlotStatus status = SlotStatus.getByValue(raw.getSlotStatus());
if (status != null) {
if (status == SlotStatus.LOCKED) {
if (OrderStatus.PATIENT_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("已退号");
} else if (OrderStatus.SYSTEM_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("系统取消");
} else {
dto.setStatus("预约");
dto.setStatus("锁定");
}
} else if (SlotStatus.RETURNED.equals(slotStatus)) {
dto.setStatus("已退号");
} else if (SlotStatus.CANCELLED.equals(slotStatus)) {
} else if (status == SlotStatus.BOOKED) {
if (OrderStatus.PATIENT_CANCELLED.getValue().equals(raw.getOrderStatus())) {
dto.setStatus("已退号");
} else {
dto.setStatus("已取号");
}
} else if (status == SlotStatus.CANCELLED) {
dto.setStatus("已停诊");
} else if (SlotStatus.LOCKED.equals(slotStatus)) {
dto.setStatus("锁定");
} else if (status == SlotStatus.RETURNED) {
dto.setStatus("退号");
} else {
dto.setStatus("未预约");
}

View File

@@ -18,6 +18,7 @@ import com.openhis.administration.mapper.PatientMapper;
import com.openhis.administration.service.*;
import com.openhis.common.constant.CommonConstants;
import com.openhis.common.constant.PromptMsgConstant;
import com.openhis.common.enums.SlotStatus;
import com.openhis.common.enums.*;
import com.openhis.common.enums.ybenums.YbPayment;
import com.openhis.common.utils.EnumUtils;
@@ -643,8 +644,7 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
.set(Order::getStatus, OrderStatus.PATIENT_CANCELLED.getValue())
.set(Order::getPayStatus, PaymentStatus.REFUND_ALL.getValue())
.set(Order::getCancelTime, new Date())
.set(Order::getCancelReason,
StringUtils.isNotEmpty(reason) ? reason : "诊前退号")
.set(Order::getCancelReason, "诊前退号")
.set(Order::getUpdateTime, new Date())
.setSql("version = version + 1")
.eq(Order::getId, appointmentOrder.getId())
@@ -660,17 +660,27 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra
return appointmentOrder.getId();
}
int slotRows = scheduleSlotMapper.updateSlotStatus(slotId, CommonConstants.SlotStatus.AVAILABLE);
if (slotRows > 0) {
Long poolId = scheduleSlotMapper.selectPoolIdBySlotId(slotId);
if (poolId != null) {
schedulePoolMapper.refreshPoolStats(poolId);
schedulePoolMapper.update(null,
new LambdaUpdateWrapper<SchedulePool>()
.setSql("version = version + 1")
.set(SchedulePool::getUpdateTime, new Date())
.eq(SchedulePool::getId, poolId));
}
// 只有已预约(1)的号源才能退号,对应签到后的 BOOKED 状态
ScheduleSlot slot = scheduleSlotMapper.selectById(slotId);
if (slot == null || !SlotStatus.BOOKED.getValue().equals(slot.getStatus())) {
log.warn("退号跳过:槽位非已预约状态, slotId={}, status={}", slotId,
slot != null ? slot.getStatus() : null);
return appointmentOrder.getId();
}
int slotRows = scheduleSlotMapper.updateSlotStatus(slotId, SlotStatus.AVAILABLE.getValue());
if (slotRows == 0) {
log.warn("退号时更新槽位状态未影响任何行, slotId={}", slotId);
return appointmentOrder.getId();
}
Long poolId = scheduleSlotMapper.selectPoolIdBySlotId(slotId);
if (poolId != null) {
schedulePoolMapper.update(null,
new LambdaUpdateWrapper<SchedulePool>()
.setSql("booked_num = booked_num - 1, version = version + 1")
.set(SchedulePool::getUpdateTime, new Date())
.eq(SchedulePool::getId, poolId));
}
return appointmentOrder.getId();
} catch (Exception e) {

View File

@@ -768,36 +768,4 @@ public class CommonConstants {
Integer ACCOUNT_DEVICE_TYPE = 6;
}
/**
* 号源槽位状态 (adm_schedule_slot.status)
*/
public interface SlotStatus {
/** 可用 / 待预约 */
Integer AVAILABLE = 0;
/** 已预约 */
Integer BOOKED = 1;
/** 已取消 / 已停诊 */
Integer CANCELLED = 2;
/** 已签到 / 已取号 */
Integer CHECKED_IN = 3;
/** 已锁定 */
Integer LOCKED = 4;
/** 已退号 */
Integer RETURNED = 5;
}
/**
* 预约订单状态 (order_main.status)
*/
public interface AppointmentOrderStatus {
/** 已预约 (待就诊) */
Integer BOOKED = 1;
/** 已取号 (已就诊) */
Integer CHECKED_IN = 2;
/** 已取消 */
Integer CANCELLED = 3;
/** 已退号 */
Integer RETURNED = 4;
}
}

View File

@@ -0,0 +1,57 @@
package com.openhis.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 号源槽位状态 (adm_schedule_slot.status)
*
* <pre>
* 状态流转:
* 预约 → 0→2 (锁定), locked_num+1
* 取消预约 → 2→0 (释放), locked_num-1
* 签到 → 2→1 (已约), locked_num-1, booked_num+1
* 退号 → 1→0 (释放), booked_num-1
* 停诊 → 任意→4 (已取消)
* </pre>
*
* @author system
*/
@Getter
@AllArgsConstructor
public enum SlotStatus implements HisEnumInterface {
/** 可用 / 待预约 */
AVAILABLE(0, "available", "可用"),
/** 已预约 */
BOOKED(1, "booked", "已预约"),
/** 已锁定 (约而不付:预约后锁定号源) */
LOCKED(2, "locked", "已锁定"),
/** 已签到 / 已取号 */
CHECKED_IN(3, "checked_in", "已签到"),
/** 已取消 / 已停诊 */
CANCELLED(4, "cancelled", "已取消"),
/** 已退号 */
RETURNED(5, "returned", "已退号");
private final Integer value;
private final String code;
private final String info;
public static SlotStatus getByValue(Integer value) {
if (value == null) {
return null;
}
for (SlotStatus val : values()) {
if (val.getValue().equals(value)) {
return val;
}
}
return null;
}
}

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());
}
}

View File

@@ -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>

View File

@@ -172,12 +172,12 @@ export const SlotStatus = {
AVAILABLE: 0,
/** 已预约 */
BOOKED: 1,
/** 已取消 / 已停诊 */
CANCELLED: 2,
/** 已锁定 */
LOCKED: 2,
/** 已签到 / 已取号 */
CHECKED_IN: 3,
/** 已锁定 */
LOCKED: 4,
/** 已取消 / 已停诊 */
CANCELLED: 4,
};
/**
@@ -185,10 +185,10 @@ export const SlotStatus = {
*/
export const SlotStatusDescriptions = {
0: '未预约',
1: '已预约',
2: '已停诊',
1: '已取号',
2: '已锁定',
3: '已取号',
4: '已锁定',
4: '已停诊',
};
/**

View File

@@ -34,6 +34,7 @@
<select id="status-select" class="search-select" v-model="selectedStatus" @change="onSearch">
<option value="all">全部</option>
<option value="unbooked">未预约</option>
<option value="locked">已锁定</option>
<option value="booked">已预约</option>
<option value="checked">已取号</option>
<option value="cancelled">已停诊</option>
@@ -253,6 +254,7 @@ import useUserStore from '@/store/modules/user';
const STATUS_CLASS_MAP = {
'未预约': 'status-unbooked',
'已锁定': 'status-locked',
'已预约': 'status-booked',
'已取号': 'status-checked',
'已退号': 'status-returned',
@@ -774,6 +776,7 @@ export default {
// 🔧 BugFix#399: 确保已取号状态正确匹配
const statusMap = {
unbooked: ['未预约'],
locked: ['已锁定'],
booked: ['已预约'],
checked: ['已取号', '已签到'],
cancelled: ['已停诊', '已取消'],

View File

@@ -1685,7 +1685,7 @@ function loadCheckInPatientList() {
const today = formatDateStr(new Date(), 'YYYY-MM-DD');
listTicket({
date: today,
status: 'booked',
status: 'locked',
name: checkInSearchKey.value, // 支持姓名等模糊查询,后端需适配
page: checkInPage.value,
limit: checkInLimit.value