diff --git a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/appservice/impl/TicketAppServiceImpl.java b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/appservice/impl/TicketAppServiceImpl.java index 573285cd..f63290d2 100644 --- a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/appservice/impl/TicketAppServiceImpl.java +++ b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/appservice/impl/TicketAppServiceImpl.java @@ -16,6 +16,7 @@ import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -123,6 +124,7 @@ public class TicketAppServiceImpl implements ITicketAppService { if (query == null) { query = new com.openhis.appointmentmanage.dto.TicketQueryDTO(); } + normalizeQueryStatus(query); // 2. 构造 MyBatis 的分页对象 (传入前端给的当前页和每页条数) com.baomidou.mybatisplus.extension.plugins.pagination.Page pageParam = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>( @@ -145,37 +147,61 @@ public class TicketAppServiceImpl implements ITicketAppService { dto.setDepartment(raw.getDepartmentName()); // 注意:以前这里传成了ID,导致前端出Bug,现在修复成了真正的科室名 dto.setFee(raw.getFee()); dto.setPatientName(raw.getPatientName()); - dto.setPatientId(raw.getPatientId() != null ? String.valueOf(raw.getPatientId()) : null); + dto.setPatientId(raw.getMedicalCard()); dto.setPhone(raw.getPhone()); + dto.setIdCard(raw.getIdCard()); + dto.setDoctorId(raw.getDoctorId()); + dto.setDepartmentId(raw.getDepartmentId()); + dto.setRealPatientId(raw.getPatientId()); + + // 性别处理:直接读取优先级最高的订单性别字段 (SQL 已处理优先级) + if (raw.getPatientGender() != null) { + String pg = raw.getPatientGender().trim(); + dto.setGender("1".equals(pg) ? "男" : ("2".equals(pg) ? "女" : "未知")); + } else { + dto.setGender("未知"); + } - // 号源类型处理 (底层是1,前端要的是expert) if (raw.getRegType() != null && raw.getRegType() == 1) { dto.setTicketType("expert"); } else { dto.setTicketType("general"); } - // 拼接就诊时间 if (raw.getScheduleDate() != null && raw.getExpectTime() != null) { dto.setDateTime(raw.getScheduleDate().toString() + " " + raw.getExpectTime().toString()); try { - dto.setAppointmentDate( - new java.text.SimpleDateFormat("yyyy-MM-dd").parse(raw.getScheduleDate().toString())); + String timeStr = raw.getAppointmentTime() != null ? raw.getAppointmentTime() : (raw.getScheduleDate().toString() + " " + raw.getExpectTime().toString()); + java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat(timeStr.length() > 10 ? "yyyy-MM-dd HH:mm" : "yyyy-MM-dd"); + java.util.Date date = sdf.parse(timeStr); + dto.setAppointmentDate(date); + dto.setAppointmentTime(date); } catch (Exception e) { dto.setAppointmentDate(new java.util.Date()); } } - // 精准状态翻译!把底层的1和2,翻译回前端能懂的中文 if (Boolean.TRUE.equals(raw.getIsStopped())) { dto.setStatus("已停诊"); } else { Integer slotStatus = raw.getSlotStatus(); if (slotStatus != null) { - if (SlotStatus.BOOKED.equals(slotStatus)) { - dto.setStatus(AppointmentOrderStatus.CHECKED_IN.equals(raw.getOrderStatus()) ? "已取号" : "已预约"); - } else if (SlotStatus.STOPPED.equals(slotStatus)) { + if (SlotStatus.CHECKED_IN.equals(slotStatus)) { + dto.setStatus("已取号"); + } else if (SlotStatus.BOOKED.equals(slotStatus)) { + if (AppointmentOrderStatus.CHECKED_IN.equals(raw.getOrderStatus())) { + dto.setStatus("已取号"); + } else if (AppointmentOrderStatus.RETURNED.equals(raw.getOrderStatus())) { + dto.setStatus("已退号"); + } else { + dto.setStatus("已预约"); + } + } else if (SlotStatus.RETURNED.equals(slotStatus)) { + dto.setStatus("已退号"); + } else if (SlotStatus.CANCELLED.equals(slotStatus)) { dto.setStatus("已停诊"); + } else if (SlotStatus.LOCKED.equals(slotStatus)) { + dto.setStatus("已锁定"); } else { dto.setStatus("未预约"); } @@ -198,6 +224,62 @@ public class TicketAppServiceImpl implements ITicketAppService { return R.ok(result); } + /** + * 统一状态入参,避免前端状态值大小写/中文/数字差异导致 SQL 条件失效后回全量数据 + */ + private void normalizeQueryStatus(com.openhis.appointmentmanage.dto.TicketQueryDTO query) { + String rawStatus = query.getStatus(); + if (rawStatus == null) { + return; + } + String normalized = rawStatus.trim(); + if (normalized.isEmpty()) { + query.setStatus(null); + return; + } + String lower = normalized.toLowerCase(Locale.ROOT); + switch (lower) { + case "all": + case "全部": + query.setStatus("all"); + break; + case "unbooked": + case "0": + case "未预约": + query.setStatus("unbooked"); + break; + case "booked": + case "1": + case "已预约": + query.setStatus("booked"); + break; + case "checked": + case "checkin": + case "checkedin": + case "2": + case "已取号": + query.setStatus("checked"); + break; + case "cancelled": + case "canceled": + case "3": + 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; + } + } + @Override public R listDoctorAvailability(com.openhis.appointmentmanage.dto.TicketQueryDTO query) { if (query == null) { @@ -242,7 +324,7 @@ public class TicketAppServiceImpl implements ITicketAppService { dto.setDepartment(raw.getDepartmentName()); dto.setFee(raw.getFee()); dto.setPatientName(raw.getPatientName()); - dto.setPatientId(raw.getPatientId() != null ? String.valueOf(raw.getPatientId()) : null); + dto.setPatientId(raw.getMedicalCard()); dto.setPhone(raw.getPhone()); // --- 号源类型处理 (普通/专家) --- @@ -258,9 +340,13 @@ public class TicketAppServiceImpl implements ITicketAppService { if (raw.getScheduleDate() != null && raw.getExpectTime() != null) { dto.setDateTime(raw.getScheduleDate().toString() + " " + raw.getExpectTime().toString()); try { - dto.setAppointmentDate( - new java.text.SimpleDateFormat("yyyy-MM-dd").parse(raw.getScheduleDate().toString())); + String timeStr = raw.getAppointmentTime() != null ? raw.getAppointmentTime() : (raw.getScheduleDate().toString() + " " + raw.getExpectTime().toString()); + java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat(timeStr.length() > 10 ? "yyyy-MM-dd HH:mm" : "yyyy-MM-dd"); + java.util.Date date = sdf.parse(timeStr); + dto.setAppointmentDate(date); + dto.setAppointmentTime(date); } catch (Exception e) { + log.error("时间解析失败", e); dto.setAppointmentDate(new java.util.Date()); } } @@ -273,10 +359,22 @@ public class TicketAppServiceImpl implements ITicketAppService { // 第二关:看独立的细分槽位状态 (0: 可用, 1: 已预约, 2: 已取消...) Integer slotStatus = raw.getSlotStatus(); if (slotStatus != null) { - if (SlotStatus.BOOKED.equals(slotStatus)) { - dto.setStatus(AppointmentOrderStatus.CHECKED_IN.equals(raw.getOrderStatus()) ? "已取号" : "已预约"); - } else if (SlotStatus.STOPPED.equals(slotStatus)) { - dto.setStatus("已停诊"); // 视业务可改回已取消 + if (SlotStatus.CHECKED_IN.equals(slotStatus)) { + dto.setStatus("已取号"); + } else if (SlotStatus.BOOKED.equals(slotStatus)) { + if (AppointmentOrderStatus.CHECKED_IN.equals(raw.getOrderStatus())) { + dto.setStatus("已取号"); + } else if (AppointmentOrderStatus.RETURNED.equals(raw.getOrderStatus())) { + dto.setStatus("已退号"); + } else { + dto.setStatus("已预约"); + } + } else if (SlotStatus.RETURNED.equals(slotStatus)) { + dto.setStatus("已退号"); + } else if (SlotStatus.CANCELLED.equals(slotStatus)) { + dto.setStatus("已停诊"); + } else if (SlotStatus.LOCKED.equals(slotStatus)) { + dto.setStatus("已锁定"); } else { dto.setStatus("未预约"); } @@ -355,15 +453,12 @@ public class TicketAppServiceImpl implements ITicketAppService { if (patient != null) { Integer genderEnum = patient.getGenderEnum(); if (genderEnum != null) { - switch (genderEnum) { - case 1: - dto.setGender("男"); - break; - case 2: - dto.setGender("女"); - break; - default: - dto.setGender("未知"); + if (Integer.valueOf(1).equals(genderEnum)) { + dto.setGender("男"); + } else if (Integer.valueOf(2).equals(genderEnum)) { + dto.setGender("女"); + } else { + dto.setGender("未知"); } } } diff --git a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/dto/TicketDto.java b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/dto/TicketDto.java index 6c3941a1..74e94643 100644 --- a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/dto/TicketDto.java +++ b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/dto/TicketDto.java @@ -99,4 +99,15 @@ public class TicketDto { */ @JsonSerialize(using = ToStringSerializer.class) private Long doctorId; + + /** + * 真实患者ID(数据库主键,区别于 patientId 存的就诊卡号) + */ + @JsonSerialize(using = ToStringSerializer.class) + private Long realPatientId; + + /** + * 身份证号 + */ + private String idCard; } diff --git a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/chargemanage/appservice/impl/OutpatientRegistrationAppServiceImpl.java b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/chargemanage/appservice/impl/OutpatientRegistrationAppServiceImpl.java index 47e55783..3db1ca2b 100644 --- a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/chargemanage/appservice/impl/OutpatientRegistrationAppServiceImpl.java +++ b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/chargemanage/appservice/impl/OutpatientRegistrationAppServiceImpl.java @@ -22,6 +22,10 @@ import com.openhis.common.enums.ybenums.YbPayment; import com.openhis.common.utils.EnumUtils; import com.openhis.common.utils.HisPageUtils; import com.openhis.common.utils.HisQueryUtils; +import com.openhis.appointmentmanage.mapper.SchedulePoolMapper; +import com.openhis.appointmentmanage.mapper.ScheduleSlotMapper; +import com.openhis.clinical.domain.Order; +import com.openhis.clinical.service.IOrderService; import com.openhis.financial.domain.PaymentReconciliation; import com.openhis.financial.domain.RefundLog; import com.openhis.financial.service.IRefundLogService; @@ -48,6 +52,7 @@ import javax.servlet.http.HttpServletRequest; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.*; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @@ -97,6 +102,15 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra @Resource IRefundLogService iRefundLogService; + @Resource + IOrderService orderService; + + @Resource + ScheduleSlotMapper scheduleSlotMapper; + + @Resource + SchedulePoolMapper schedulePoolMapper; + /** * 门诊挂号 - 查询患者信息 * @@ -291,6 +305,11 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra } } + // 如果本次门诊挂号来自预约签到,同步把预约订单与号源槽位状态改为已退号 + if (result != null && result.getCode() == 200) { + syncAppointmentReturnStatus(byId, cancelRegPaymentDto.getReason()); + } + // 记录退号日志 recordRefundLog(cancelRegPaymentDto, byId, result, paymentRecon); @@ -399,6 +418,74 @@ public class OutpatientRegistrationAppServiceImpl implements IOutpatientRegistra return R.ok("已取消挂号"); } + /** + * 同步预约号源状态为已退号。 + * 说明: + * 1) 门诊退号主流程不依赖该步骤成功与否,因此此方法内部异常仅记录日志,不向上抛出。 + * 2) 通过患者、科室、日期以及状态筛选最近一条预约订单,尽量避免误匹配。 + */ + private void syncAppointmentReturnStatus(Encounter encounter, String reason) { + if (encounter == null || encounter.getPatientId() == null) { + return; + } + try { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .eq(Order::getPatientId, encounter.getPatientId()) + .in(Order::getStatus, CommonConstants.AppointmentOrderStatus.BOOKED, + CommonConstants.AppointmentOrderStatus.CHECKED_IN) + .orderByDesc(Order::getUpdateTime) + .orderByDesc(Order::getCreateTime) + .last("LIMIT 1"); + + if (encounter.getOrganizationId() != null) { + queryWrapper.eq(Order::getDepartmentId, encounter.getOrganizationId()); + } + if (encounter.getTenantId() != null) { + queryWrapper.eq(Order::getTenantId, encounter.getTenantId()); + } + if (encounter.getCreateTime() != null) { + LocalDate encounterDate = encounter.getCreateTime().toInstant() + .atZone(ZoneId.systemDefault()).toLocalDate(); + Date startOfDay = Date.from(encounterDate.atStartOfDay(ZoneId.systemDefault()).toInstant()); + Date nextDayStart = Date.from(encounterDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant()); + queryWrapper.ge(Order::getAppointmentDate, startOfDay) + .lt(Order::getAppointmentDate, nextDayStart); + } + + Order appointmentOrder = orderService.getOne(queryWrapper, false); + if (appointmentOrder == null) { + return; + } + + Date now = new Date(); + if (!CommonConstants.AppointmentOrderStatus.RETURNED.equals(appointmentOrder.getStatus())) { + Order updateOrder = new Order(); + updateOrder.setId(appointmentOrder.getId()); + updateOrder.setStatus(CommonConstants.AppointmentOrderStatus.RETURNED); + updateOrder.setCancelTime(now); + updateOrder.setCancelReason( + StringUtils.isNotEmpty(reason) ? reason : "门诊退号"); + updateOrder.setUpdateTime(now); + orderService.updateById(updateOrder); + } + + Long slotId = appointmentOrder.getSlotId(); + if (slotId == null) { + return; + } + + int slotRows = scheduleSlotMapper.updateSlotStatus(slotId, CommonConstants.SlotStatus.RETURNED); + if (slotRows > 0) { + Long poolId = scheduleSlotMapper.selectPoolIdBySlotId(slotId); + if (poolId != null) { + schedulePoolMapper.refreshPoolStats(poolId); + } + } + } catch (Exception e) { + log.warn("同步预约号源已退号状态失败, encounterId={}", encounter.getId(), e); + } + } + /** * 补打挂号 * 补打挂号不需要修改数据库,只需要返回成功即可,前端已有所有需要的数据用于打印 diff --git a/openhis-server-new/openhis-common/src/main/java/com/openhis/common/constant/CommonConstants.java b/openhis-server-new/openhis-common/src/main/java/com/openhis/common/constant/CommonConstants.java index 40313859..1521d552 100644 --- a/openhis-server-new/openhis-common/src/main/java/com/openhis/common/constant/CommonConstants.java +++ b/openhis-server-new/openhis-common/src/main/java/com/openhis/common/constant/CommonConstants.java @@ -769,15 +769,21 @@ public class CommonConstants { } /** - * 号源槽位状态 (adm_schedule_slot.slot_status) + * 号源槽位状态 (adm_schedule_slot.status) */ public interface SlotStatus { /** 可用 / 待预约 */ Integer AVAILABLE = 0; /** 已预约 */ Integer BOOKED = 1; - /** 已停诊 / 已失效 */ - Integer STOPPED = 2; + /** 已取消 / 已停诊 */ + Integer CANCELLED = 2; + /** 已锁定 */ + Integer LOCKED = 3; + /** 已签到 / 已取号 */ + Integer CHECKED_IN = 4; + /** 已退号 */ + Integer RETURNED = 5; } /** @@ -790,6 +796,8 @@ public class CommonConstants { Integer CHECKED_IN = 2; /** 已取消 */ Integer CANCELLED = 3; + /** 已退号 */ + Integer RETURNED = 4; } } diff --git a/openhis-server-new/openhis-domain/src/main/java/com/openhis/appointmentmanage/domain/ScheduleSlot.java b/openhis-server-new/openhis-domain/src/main/java/com/openhis/appointmentmanage/domain/ScheduleSlot.java index de6af5a4..e3185138 100644 --- a/openhis-server-new/openhis-domain/src/main/java/com/openhis/appointmentmanage/domain/ScheduleSlot.java +++ b/openhis-server-new/openhis-domain/src/main/java/com/openhis/appointmentmanage/domain/ScheduleSlot.java @@ -9,6 +9,8 @@ import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; import java.time.LocalTime; +import java.util.Date; + /** * 号源池明细Entity * @@ -29,7 +31,7 @@ public class ScheduleSlot extends HisBaseEntity { /** 序号 */ private Integer seqNo; - /** 序号状态: 0-可用,1-已预约,2-已取消,3-已过期等 */ + /** 序号状态: 0-可用,1-已预约,2-已取消/已停诊,3-已锁定,4-已签到,5-已退号 */ private Integer status; /** 预约订单ID */ @@ -37,4 +39,7 @@ public class ScheduleSlot extends HisBaseEntity { /** 预计叫号时间 */ private LocalTime expectTime; + + /** 签到时间 */ + private Date checkInTime; } diff --git a/openhis-server-new/openhis-domain/src/main/java/com/openhis/appointmentmanage/domain/TicketSlotDTO.java b/openhis-server-new/openhis-domain/src/main/java/com/openhis/appointmentmanage/domain/TicketSlotDTO.java index 96377d1b..351d1015 100644 --- a/openhis-server-new/openhis-domain/src/main/java/com/openhis/appointmentmanage/domain/TicketSlotDTO.java +++ b/openhis-server-new/openhis-domain/src/main/java/com/openhis/appointmentmanage/domain/TicketSlotDTO.java @@ -22,6 +22,13 @@ public class TicketSlotDTO { private Long patientId; private String phone; private Integer orderStatus; + private Long orderId; + private String orderNo; + private String patientGender; + private Integer genderEnum; + private String idCard; + private String encounterId; + private String appointmentTime; // 底层逻辑判断专属字段 private Integer slotStatus; diff --git a/openhis-server-new/openhis-domain/src/main/java/com/openhis/appointmentmanage/mapper/SchedulePoolMapper.java b/openhis-server-new/openhis-domain/src/main/java/com/openhis/appointmentmanage/mapper/SchedulePoolMapper.java index 77927d87..52987380 100644 --- a/openhis-server-new/openhis-domain/src/main/java/com/openhis/appointmentmanage/mapper/SchedulePoolMapper.java +++ b/openhis-server-new/openhis-domain/src/main/java/com/openhis/appointmentmanage/mapper/SchedulePoolMapper.java @@ -37,4 +37,21 @@ public interface SchedulePoolMapper extends BaseMapper { AND p.delete_flag = '0' """) int refreshPoolStats(@Param("poolId") Long poolId); + + /** + * 签到时更新号源池统计:锁定数-1,已预约数+1 + * + * @param poolId 号源池ID + * @return 结果 + */ + @Update(""" + UPDATE adm_schedule_pool + SET locked_num = locked_num - 1, + booked_num = booked_num + 1, + update_time = NOW() + WHERE id = #{poolId} + AND locked_num > 0 + AND delete_flag = '0' + """) + int updatePoolStatsOnCheckIn(@Param("poolId") Integer poolId); } diff --git a/openhis-server-new/openhis-domain/src/main/java/com/openhis/appointmentmanage/mapper/ScheduleSlotMapper.java b/openhis-server-new/openhis-domain/src/main/java/com/openhis/appointmentmanage/mapper/ScheduleSlotMapper.java index 805e342f..8d2e4966 100644 --- a/openhis-server-new/openhis-domain/src/main/java/com/openhis/appointmentmanage/mapper/ScheduleSlotMapper.java +++ b/openhis-server-new/openhis-domain/src/main/java/com/openhis/appointmentmanage/mapper/ScheduleSlotMapper.java @@ -6,6 +6,7 @@ import com.openhis.appointmentmanage.domain.ScheduleSlot; import com.openhis.appointmentmanage.domain.TicketSlotDTO; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.openhis.appointmentmanage.dto.TicketQueryDTO; +import java.util.Date; import java.util.List; import org.springframework.stereotype.Repository; import org.apache.ibatis.annotations.Param; @@ -30,6 +31,16 @@ public interface ScheduleSlotMapper extends BaseMapper { */ int updateSlotStatus(@Param("slotId") Long slotId, @Param("status") Integer status); + /** + * 更新槽位状态并记录签到时间 + * + * @param slotId 槽位ID + * @param status 状态 + * @param checkInTime 签到时间 + * @return 结果 + */ + int updateSlotStatusAndCheckInTime(@Param("slotId") Long slotId, @Param("status") Integer status, @Param("checkInTime") Date checkInTime); + /** * 根据槽位ID查询所属号源池ID。 */ diff --git a/openhis-server-new/openhis-domain/src/main/java/com/openhis/clinical/domain/Order.java b/openhis-server-new/openhis-domain/src/main/java/com/openhis/clinical/domain/Order.java index 2bd02dee..456d4d34 100644 --- a/openhis-server-new/openhis-domain/src/main/java/com/openhis/clinical/domain/Order.java +++ b/openhis-server-new/openhis-domain/src/main/java/com/openhis/clinical/domain/Order.java @@ -20,7 +20,7 @@ import lombok.experimental.Accessors; @EqualsAndHashCode(callSuper = false) public class Order extends HisBaseEntity { - @TableId(type = IdType.ASSIGN_ID) + @TableId(type = IdType.AUTO) @JsonSerialize(using = ToStringSerializer.class) private Long id; diff --git a/openhis-server-new/openhis-domain/src/main/java/com/openhis/clinical/mapper/OrderMapper.java b/openhis-server-new/openhis-domain/src/main/java/com/openhis/clinical/mapper/OrderMapper.java index 8a27dc08..b5fab5da 100644 --- a/openhis-server-new/openhis-domain/src/main/java/com/openhis/clinical/mapper/OrderMapper.java +++ b/openhis-server-new/openhis-domain/src/main/java/com/openhis/clinical/mapper/OrderMapper.java @@ -32,4 +32,14 @@ public interface OrderMapper extends BaseMapper { int updateOrderStatusById(Long id, Integer status); int updateOrderCancelInfoById(Long id, Date cancelTime, String cancelReason); + + /** + * 更新订单支付状态 + * + * @param orderId 订单ID + * @param payStatus 支付状态:0-未支付,1-已支付 + * @param payTime 支付时间 + * @return 结果 + */ + int updatePayStatus(@Param("orderId") Long orderId, @Param("payStatus") Integer payStatus, @Param("payTime") Date payTime); } diff --git a/openhis-server-new/openhis-domain/src/main/java/com/openhis/clinical/service/impl/OrderServiceImpl.java b/openhis-server-new/openhis-domain/src/main/java/com/openhis/clinical/service/impl/OrderServiceImpl.java index 27c699b1..79a62992 100644 --- a/openhis-server-new/openhis-domain/src/main/java/com/openhis/clinical/service/impl/OrderServiceImpl.java +++ b/openhis-server-new/openhis-domain/src/main/java/com/openhis/clinical/service/impl/OrderServiceImpl.java @@ -88,6 +88,7 @@ public class OrderServiceImpl extends ServiceImpl implements } Order order = new Order(); + order.setId(null); // 显式置空,确保触发数据库自增,避免 MP 预分配雪花 ID 的干扰 String orderNo = assignSeqUtil.getSeq(AssignSeqEnum.ORDER_NUM.getPrefix(), 18); order.setOrderNo(orderNo); diff --git a/openhis-server-new/openhis-domain/src/main/java/com/openhis/clinical/service/impl/TicketServiceImpl.java b/openhis-server-new/openhis-domain/src/main/java/com/openhis/clinical/service/impl/TicketServiceImpl.java index b1ffbfbe..c8f0f5f9 100644 --- a/openhis-server-new/openhis-domain/src/main/java/com/openhis/clinical/service/impl/TicketServiceImpl.java +++ b/openhis-server-new/openhis-domain/src/main/java/com/openhis/clinical/service/impl/TicketServiceImpl.java @@ -5,6 +5,7 @@ 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.ScheduleSlot; import com.openhis.appointmentmanage.mapper.SchedulePoolMapper; import com.openhis.appointmentmanage.mapper.ScheduleSlotMapper; import com.openhis.clinical.domain.Order; @@ -52,6 +53,9 @@ public class TicketServiceImpl extends ServiceImpl impleme @Resource private SchedulePoolMapper schedulePoolMapper; + @Resource + private com.openhis.clinical.mapper.OrderMapper orderMapper; + @Resource private IAppointmentConfigService appointmentConfigService; @@ -277,7 +281,7 @@ public class TicketServiceImpl extends ServiceImpl impleme } /** - * 取号 + * 取号(签到) * * @param slotId 槽位ID * @return 结果 @@ -290,7 +294,24 @@ public class TicketServiceImpl extends ServiceImpl impleme throw new RuntimeException("当前号源没有可取号的预约订单"); } Order latestOrder = orders.get(0); - return orderService.updateOrderStatusById(latestOrder.getId(), AppointmentOrderStatus.CHECKED_IN); + + // 1. 更新订单状态为已取号,并更新支付状态和支付时间 + orderService.updateOrderStatusById(latestOrder.getId(), AppointmentOrderStatus.CHECKED_IN); + // 更新支付状态为已支付,记录支付时间 + orderMapper.updatePayStatus(latestOrder.getId(), 1, new Date()); + + // 2. 查询号源槽位信息 + ScheduleSlot slot = scheduleSlotMapper.selectById(slotId); + + // 3. 更新号源槽位状态为已签到,记录签到时间 + scheduleSlotMapper.updateSlotStatusAndCheckInTime(slotId, SlotStatus.CHECKED_IN, new Date()); + + // 4. 更新号源池统计:锁定数-1,已预约数+1 + if (slot != null && slot.getPoolId() != null) { + schedulePoolMapper.updatePoolStatsOnCheckIn(slot.getPoolId()); + } + + return 1; } /** @@ -312,7 +333,7 @@ public class TicketServiceImpl extends ServiceImpl impleme orderService.cancelAppointmentOrder(order.getId(), "医生停诊"); } - int updated = scheduleSlotMapper.updateSlotStatus(slotId, SlotStatus.STOPPED); + int updated = scheduleSlotMapper.updateSlotStatus(slotId, SlotStatus.CANCELLED); if (updated > 0) { refreshPoolStatsBySlotId(slotId); } diff --git a/openhis-server-new/openhis-domain/src/main/resources/mapper/administration/ScheduleSlotMapper.xml b/openhis-server-new/openhis-domain/src/main/resources/mapper/administration/ScheduleSlotMapper.xml index 2df3176c..7f50bfe3 100644 --- a/openhis-server-new/openhis-domain/src/main/resources/mapper/administration/ScheduleSlotMapper.xml +++ b/openhis-server-new/openhis-domain/src/main/resources/mapper/administration/ScheduleSlotMapper.xml @@ -4,6 +4,41 @@ "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> + + + 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 ('3', 'locked') THEN 3 + WHEN LOWER(CONCAT('', s.status)) IN ('4', 'checked', 'checked_in', 'checkin') THEN 4 + WHEN LOWER(CONCAT('', s.status)) IN ('5', 'returned') THEN 5 + ELSE NULL + END + + + + CASE + WHEN LOWER(CONCAT('', o.status)) IN ('1', 'booked') THEN 1 + WHEN LOWER(CONCAT('', o.status)) IN ('2', 'checked', 'checked_in', 'checkin') THEN 2 + WHEN LOWER(CONCAT('', o.status)) IN ('3', 'cancelled', 'canceled') THEN 3 + WHEN LOWER(CONCAT('', o.status)) IN ('4', 'returned') THEN 4 + ELSE NULL + END + + + + 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 ('3', 'locked') THEN 3 + WHEN LOWER(CONCAT('', p.status)) IN ('4', 'checked', 'checked_in', 'checkin') THEN 4 + WHEN LOWER(CONCAT('', p.status)) IN ('5', 'returned') THEN 5 + ELSE NULL + END + + @@ -115,7 +161,7 @@ UPDATE adm_schedule_slot SET status = #{status}, - + order_id = NULL, update_time = now() @@ -124,11 +170,25 @@ AND delete_flag = '0' + + UPDATE adm_schedule_slot + SET + status = #{status}, + check_in_time = #{checkInTime}, + update_time = NOW() + WHERE + id = #{slotId} + AND delete_flag = '0' + + @@ -155,12 +215,18 @@ o.patient_name AS patientName, o.medical_card AS medicalCard, o.phone AS phone, - o.status AS orderStatus, - s.status AS slotStatus, + o.id AS orderId, + o.order_no AS orderNo, + COALESCE(CAST(o.gender AS VARCHAR), CAST(pinfo.gender_enum AS VARCHAR)) AS patientGender, + pinfo.gender_enum AS genderEnum, + pinfo.id_card AS idCard, + o.appointment_time AS appointmentTime, + AS orderStatus, + AS slotStatus, s.expect_time AS expectTime, p.schedule_date AS scheduleDate, d.reg_type AS regType, - p.status AS poolStatus, + AS poolStatus, p.stop_reason AS stopReason, d.is_stopped AS isStopped FROM @@ -176,15 +242,20 @@ patient_name, medical_card, phone, + id, + order_no, + gender, + appointment_time, status FROM order_main WHERE - status IN (1, 2) + LOWER(CONCAT('', status)) IN ('1', '2', '4', 'booked', 'checked', 'checked_in', 'checkin', 'returned') ORDER BY slot_id, create_time DESC ) o ON o.slot_id = s.id + LEFT JOIN adm_patient pinfo ON o.patient_id = pinfo.id p.delete_flag = '0' AND s.delete_flag = '0' @@ -225,35 +296,49 @@ - - AND s.status = 0 + + AND = 0 AND ( d.is_stopped IS NULL OR d.is_stopped = FALSE ) - - AND s.status = 1 - AND o.status = 1 + + AND = 1 + AND = 1 AND ( d.is_stopped IS NULL OR d.is_stopped = FALSE ) - - AND s.status = 1 - AND o.status = 2 + + AND ( + = 4 + OR ( + = 1 + AND = 2 + ) + ) AND ( d.is_stopped IS NULL OR d.is_stopped = FALSE ) - + AND ( - s.status = 2 + = 2 OR d.is_stopped = TRUE ) + + AND ( + = 5 + OR = 4 + ) + + + AND 1 = 2 + @@ -266,9 +351,22 @@ SELECT p.doctor_id AS doctorId, p.doctor_name AS doctorName, - COALESCE(SUM(GREATEST(COALESCE(p.total_quota, 0) - COALESCE(p.booked_num, 0) - COALESCE(p.locked_num, 0), 0)), 0) AS available, + COALESCE( + SUM( + GREATEST( + COALESCE(p.total_quota, 0) - COALESCE(p.booked_num, 0) - COALESCE(p.locked_num, 0), + 0 + ) + ), + 0 + ) AS available, CASE - WHEN MAX(CASE WHEN d.reg_type = 1 THEN 1 ELSE 0 END) = 1 THEN 'expert' + WHEN MAX( + CASE + WHEN d.reg_type = 1 THEN 1 + ELSE 0 + END + ) = 1 THEN 'expert' ELSE 'general' END AS ticketType FROM @@ -290,7 +388,10 @@ AND d.reg_type = 1 - AND (d.reg_type != 1 OR d.reg_type IS NULL) + AND ( + d.reg_type != 1 + OR d.reg_type IS NULL + ) diff --git a/openhis-server-new/openhis-domain/src/main/resources/mapper/clinical/OrderMapper.xml b/openhis-server-new/openhis-domain/src/main/resources/mapper/clinical/OrderMapper.xml index 593d1165..52580888 100644 --- a/openhis-server-new/openhis-domain/src/main/resources/mapper/clinical/OrderMapper.xml +++ b/openhis-server-new/openhis-domain/src/main/resources/mapper/clinical/OrderMapper.xml @@ -127,7 +127,7 @@ select * from order_main where slot_id = #{slotId} and status = 1 - + insert into order_main order_no, @@ -225,6 +225,12 @@ update order_main set status = 3, cancel_time = #{cancelTime}, cancel_reason = #{cancelReason} where id = #{id} + + update order_main + set pay_status = #{payStatus}, pay_time = #{payTime}, update_time = NOW() + where id = #{orderId} + + delete from order_main where id = #{id} diff --git a/openhis-ui-vue3/src/utils/medicalConstants.js b/openhis-ui-vue3/src/utils/medicalConstants.js index ac6de2c1..809947ca 100644 --- a/openhis-ui-vue3/src/utils/medicalConstants.js +++ b/openhis-ui-vue3/src/utils/medicalConstants.js @@ -162,3 +162,61 @@ export const STATUS = { NORMAL: '0', // 正常/启用 DISABLE: '1' // 停用 }; + +/** + * 号源槽位状态(与后端 CommonConstants.SlotStatus 保持一致) + * adm_schedule_slot.status 字段 + */ +export const SlotStatus = { + /** 可用 / 待预约 */ + AVAILABLE: 0, + /** 已预约 */ + BOOKED: 1, + /** 已取消 / 已停诊 */ + CANCELLED: 2, + /** 已锁定 */ + LOCKED: 3, + /** 已签到 / 已取号 */ + CHECKED_IN: 4, +}; + +/** + * 号源槽位状态说明信息 + */ +export const SlotStatusDescriptions = { + 0: '未预约', + 1: '已预约', + 2: '已停诊', + 3: '已锁定', + 4: '已取号', +}; + +/** + * 号源槽位状态对应的CSS类名 + */ +export const SlotStatusClassMap = { + '未预约': 'status-unbooked', + '已预约': 'status-booked', + '已取号': 'status-checked', + '已停诊': 'status-cancelled', + '已取消': 'status-cancelled', + '已锁定': 'status-locked', +}; + +/** + * 获取号源槽位状态的说明 + * @param {number} value - 状态值 + * @returns {string} - 说明信息 + */ +export function getSlotStatusDescription(value) { + return SlotStatusDescriptions[value] || '未知状态'; +} + +/** + * 获取号源槽位状态对应的CSS类名 + * @param {string} status - 状态说明 + * @returns {string} - CSS类名 + */ +export function getSlotStatusClass(status) { + return SlotStatusClassMap[status] || 'status-unbooked'; +} diff --git a/openhis-ui-vue3/src/views/appoinmentmanage/outpatientAppointment/index.vue b/openhis-ui-vue3/src/views/appoinmentmanage/outpatientAppointment/index.vue index 00bb3a11..aa36accd 100644 --- a/openhis-ui-vue3/src/views/appoinmentmanage/outpatientAppointment/index.vue +++ b/openhis-ui-vue3/src/views/appoinmentmanage/outpatientAppointment/index.vue @@ -37,6 +37,7 @@ + @@ -632,7 +701,8 @@ import { returnRegister, updatePatientPhone, } from './components/outpatientregistration'; -import {invokeYbPlugin5000, invokeYbPlugin5001} from '@/api/public'; +import { listTicket, checkInTicket } from '@/api/appoinmentmanage/ticket'; +import { invokeYbPlugin5000, invokeYbPlugin5001 } from '@/api/public'; import patientInfoDialog from './components/patientInfoDialog'; import PatientAddDialog from './components/patientAddDialog'; import patientList from './components/patientList'; @@ -644,7 +714,7 @@ import {handleColor} from '@/utils/his'; import useUserStore from '@/store/modules/user'; import {formatDateStr} from '@/utils/index'; import {isValidCNPhoneNumber} from '../../../utils/validate'; -import {ElMessage} from 'element-plus'; +import {ElMessage, ElMessageBox} from 'element-plus'; import {hiprint} from 'vue-plugin-hiprint'; import outpatientRegistrationTemplate from '@/components/Print/OutpatientRegistration.json'; @@ -687,14 +757,25 @@ const ybTypeRef = ref(null); const openDialog = ref(false); const openRefundDialog = ref(false); const openReprintDialog = ref(false); + +// 预约签到相关变量 +const showCheckInPatientModal = ref(false); +const checkInPatientList = ref([]); +const selectedCheckInPatient = ref(null); const totalAmount = ref(0); const chargeItemIdList = ref([]); const chrgBchnoList = ref([]); const paymentId = ref(''); const loadingText = ref(''); +const checkInSearchKey = ref(''); +const checkInPage = ref(1); +const checkInLimit = ref(10); +const checkInTotal = ref(0); +const checkInLoading = ref(false); const registerInfo = ref({}); // 原挂号记录信息 const queryType = ref('all'); // 查询类型:all-全部, normal-正常挂号, returned-退号记录 const guardianAgeConfig = ref(''); // 监护人规定年龄配置 +const currentSlotId = ref(null); // 当前预约签到的号源ID // 使用 ref 定义查询所得用户信息数据 const patientInfoList = ref(undefined); @@ -1584,6 +1665,189 @@ function handleReprint() { openReprintDialog.value = true; } +/** 预约签到 - 打开患者选择弹窗 */ +function handleCheckIn() { + // 打开患者选择弹窗,显示已预约但未签到的患者列表 + showCheckInPatientModal.value = true; + // 加载已预约未签到的患者列表 + loadCheckInPatientList(); +} + +/** 加载预约签到患者列表 */ +function loadCheckInPatientList() { + checkInLoading.value = true; + const today = formatDateStr(new Date(), 'YYYY-MM-DD'); + listTicket({ + date: today, + status: 'booked', + name: checkInSearchKey.value, // 支持姓名等模糊查询,后端需适配 + page: checkInPage.value, + limit: checkInLimit.value + }).then(res => { + const data = res.data?.list || res.list || res.data || []; + const total = res.data?.total || res.total || data.length; + + checkInPatientList.value = data.map(item => ({ + ...item, + appointmentDate: item.scheduleDate + ' ' + (item.expectTime || '') + })); + checkInTotal.value = total; + }).catch(err => { + console.error('加载预约导出失败:', err); + ElMessage.error('获取预约列表失败'); + }).finally(() => { + checkInLoading.value = false; + }); +} + +/** 弹窗行点击处理 */ +function selectRow(row) { + selectedCheckInPatient.value = row; +} + +/** 确认签到(一键签到:直接构建挂号参数 → 预结算 → 弹收费窗口) */ +async function confirmCheckIn() { + if (!selectedCheckInPatient.value) { + ElMessage.warning('请先选择患者'); + return; + } + + const patient = selectedCheckInPatient.value; + // 每次开始新的签到流程先清理残留 slotId,避免历史脏值串单 + currentSlotId.value = null; + + // 弹出确认提示 + try { + await ElMessageBox.confirm( + `确认为患者【${patient.patientName}】办理签到挂号?\n` + + `科室:${patient.department || '-'}\n` + + `医生:${patient.doctor || '-'}\n` + + `费用:¥${patient.fee || '0.00'}`, + '签到确认', + { + confirmButtonText: '确认签到', + cancelButtonText: '取消', + type: 'info', + } + ); + } catch { + // 用户点了取消 + return; + } + + showCheckInPatientModal.value = false; + + readCardLoading.value = true; + loadingText.value = '正在处理签到挂号...'; + + try { + // 1. 用科室ID加载该科室的挂号类型列表,获取 serviceTypeId 和 definitionId + const healthcareRes = await getHealthcareMetadata({ organizationId: patient.departmentId }); + const healthcareRecords = healthcareRes.data?.records || []; + + if (healthcareRecords.length === 0) { + ElMessage.error('该科室未配置挂号类型,无法自动签到'); + readCardLoading.value = false; + return; + } + + // 2. 按号源类型(专家/普通)模糊匹配挂号类型 + const matchTypeName = (patient.ticketType === 'expert') ? '专家' : '普通'; + const matchedService = healthcareRecords.find(h => h.name && h.name.includes(matchTypeName)); + + if (!matchedService) { + // 匹配不到就取第一个作为兜底 + ElMessage.warning('未精确匹配到挂号类型,已使用默认类型'); + } + + const service = matchedService || healthcareRecords[0]; + const realPatientId = patient.realPatientId; // 后端新增的真实患者数据库ID + + if (!realPatientId) { + ElMessage.error('患者ID缺失,请联系管理员检查预约数据'); + readCardLoading.value = false; + return; + } + + // 3. 构建挂号参数(与 transformFormData 结构一致) + const registrationParam = { + encounterFormData: { + patientId: realPatientId, + priorityEnum: 3, // 默认优先级 + serviceTypeId: service.id, + organizationId: patient.departmentId, + }, + encounterLocationFormData: { + locationId: null, + }, + encounterParticipantFormData: { + practitionerId: patient.doctorId, + }, + accountFormData: { + patientId: realPatientId, + typeCode: 1, // 个人现金账户 + contractNo: '0000', // 默认自费 + }, + chargeItemFormData: { + patientId: realPatientId, + definitionId: service.definitionId, + serviceId: service.id, + totalPrice: parseFloat(patient.fee) || ((service.price || 0) + (service.activityPrice || 0)), + }, + }; + + // 4. 设置 patientInfo(ChargeDialog 需要展示) + patientInfo.value = { + patientId: realPatientId, + patientName: patient.patientName, + genderEnum_enumText: patient.gender || '-', + age: '', + contractName: '自费', + idCard: patient.idCard, + phone: patient.phone, + categoryEnum: '门诊', + organizationName: patient.department || '', + practitionerName: patient.doctor || '', + healthcareName: service.name || '', + }; + + // 同步设置 form 的 contractNo,ChargeDialog 的 feeType 会读取它 + form.value.contractNo = '0000'; + + // 5. 调用预结算接口(reg-pre-pay) + const res = await addOutpatientRegistration(registrationParam); + + if (res.code == 200) { + // 仅在预结算成功后记录待签到的号源,避免失败路径残留脏数据 + currentSlotId.value = patient.slot_id; + + // 6. 设置收费弹窗所需的数据 + chrgBchno.value = res.data.chrgBchno; + registerBusNo.value = res.data.busNo; + totalAmount.value = res.data.psnCashPay; + patientInfo.value.encounterId = res.data.encounterId || ''; + patientInfo.value.busNo = res.data.busNo || ''; + transformedData.value = registrationParam; + chargeItemIdList.value = []; + + // 7. 打开收费弹窗 + openDialog.value = true; + + // 打印挂号单 + printRegistrationByHiprint(res.data); + } else { + currentSlotId.value = null; + ElMessage.error(res.msg || '预结算失败'); + } + } catch (err) { + currentSlotId.value = null; + console.error('预约签到失败:', err); + ElMessage.error('签到处理失败: ' + (err.message || '未知错误')); + } finally { + readCardLoading.value = false; + } +} + /** * 点击患者列表给表单赋值 */ @@ -1656,20 +1920,29 @@ function handleClose(value) { proxy.$modal.msgSuccess('操作成功'); // 更新患者手机号 updatePhone(); - // getList(); - // reset(); - // addOutpatientRegistration(transformedData.value).then((response) => { - // reset(); - // proxy.$modal.msgSuccess('新增成功'); - // getList(); - // }); + + // 先取出并清空,避免接口失败/取消等路径导致 slotId 残留污染下一单 + const pendingSlotId = currentSlotId.value; + currentSlotId.value = null; + + // 如果是预约签到的挂号,执行签到状态更新 + if (pendingSlotId) { + checkInTicket(pendingSlotId).then(() => { + console.log('预约状态已更新为已取号'); + }).catch(err => { + console.error('更新预约状态失败:', err); + ElMessage.error('预约状态更新失败,请手动签到'); + }); + } } else if (value == 'cancel') { + currentSlotId.value = null; // cancelRegister(patientInfo.value.encounterId).then((res) => { // if (res.code == 200) { // getList(); // } // }); } else { + currentSlotId.value = null; openRefundDialog.value = false; } }