From cb46461edece1beac570b20daca5768edce3dd33 Mon Sep 17 00:00:00 2001 From: his-dev Date: Fri, 3 Apr 2026 14:08:23 +0800 Subject: [PATCH 01/13] =?UTF-8?q?fix(#303):=20=E5=B0=86=E5=8F=96=E6=B6=88?= =?UTF-8?q?=E9=A2=84=E7=BA=A6=E9=99=90=E5=88=B6=E4=BB=8E=E5=8F=96=E6=B6=88?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E7=A7=BB=E8=87=B3=E9=A2=84=E7=BA=A6=E6=8C=82?= =?UTF-8?q?=E5=8F=B7=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:取消预约时检查次数限制,导致用户无法取消预约 修复:将取消次数限制检查移到预约挂号时进行 变更: - bookTicket(): 添加取消次数限制检查,达到上限禁止预约 - cancelTicket(): 移除取消限制检查,允许正常取消 提示信息:"由于您在月度内累计取消预约已达X次,触发系统限制,暂时无法在线预约,请联系分诊台或咨询客服。" --- .../service/impl/TicketServiceImpl.java | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) 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 01fb025e..b1ffbfbe 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 @@ -146,7 +146,25 @@ public class TicketServiceImpl extends ServiceImpl impleme logger.debug("开始执行纯净打单路线,slotId: {}, patientName: {}", slotId, dto.getPatientName()); - // 1. 直查物理大底座! + // 1. 检查患者取消预约次数限制(应在预约挂号时限制,而非取消预约时) + Integer tenantId = dto.getTenant_id(); + Long patientId = dto.getPatientId(); + if (tenantId != null && patientId != null) { + AppointmentConfig config = appointmentConfigService.getConfigByTenantId(tenantId); + if (config != null && config.getCancelAppointmentCount() != null + && config.getCancelAppointmentCount() > 0) { + // 计算当前周期的起始时间 + LocalDateTime startTime = calculatePeriodStartTime(config.getCancelAppointmentType()); + // 统计已取消次数 + long cancelledCount = orderService.countPatientCancellations(patientId, tenantId, startTime); + if (cancelledCount >= config.getCancelAppointmentCount()) { + String periodName = getPeriodName(config.getCancelAppointmentType()); + throw new RuntimeException("由于您在" + periodName + "内累计取消预约已达" + cancelledCount + "次,触发系统限制,暂时无法在线预约,请联系分诊台或咨询客服。"); + } + } + } + + // 2. 直查物理大底座! TicketSlotDTO slot = scheduleSlotMapper.selectTicketSlotById(slotId); if (slot == null) { @@ -242,26 +260,11 @@ public class TicketServiceImpl extends ServiceImpl impleme throw new RuntimeException("当前号源没有可取消的预约订单"); } - // 核心逻辑:获取订单信息并检查机构取消限制 + // 获取订单信息 Order latestOrder = orders.get(0); - Integer tenantId = latestOrder.getTenantId(); - Long patientId = latestOrder.getPatientId(); - - if (tenantId != null && patientId != null) { - AppointmentConfig config = appointmentConfigService.getConfigByTenantId(tenantId); - if (config != null && config.getCancelAppointmentCount() != null - && config.getCancelAppointmentCount() > 0) { - // 计算当前周期的起始时间 - LocalDateTime startTime = calculatePeriodStartTime(config.getCancelAppointmentType()); - // 统计已取消次数 - long cancelledCount = orderService.countPatientCancellations(patientId, tenantId, startTime); - if (cancelledCount >= config.getCancelAppointmentCount()) { - String periodName = getPeriodName(config.getCancelAppointmentType()); - throw new RuntimeException("您在" + periodName + "内已达到该机构取消预约次数上限(" + config.getCancelAppointmentCount() + "次),禁止取消"); - } - } - } + // 直接执行取消,不再检查取消限制 + // 根据需求,取消限制应在预约挂号时检查,而非取消预约时 for (Order order : orders) { orderService.cancelAppointmentOrder(order.getId(), "患者取消预约"); } From 1b3d4e3dc05d09afdfdf1fb690c2d8cb6809b98a Mon Sep 17 00:00:00 2001 From: HuangXinQuan Date: Fri, 3 Apr 2026 14:42:13 +0800 Subject: [PATCH 02/13] =?UTF-8?q?77=20=E9=97=A8=E8=AF=8A=E6=8C=82=E5=8F=B7?= =?UTF-8?q?-=E3=80=8B=E9=A2=84=E7=BA=A6=E7=AD=BE=E5=88=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../appservice/impl/TicketAppServiceImpl.java | 145 +++++++-- .../web/appointmentmanage/dto/TicketDto.java | 11 + .../OutpatientRegistrationAppServiceImpl.java | 87 ++++++ .../common/constant/CommonConstants.java | 14 +- .../domain/ScheduleSlot.java | 7 +- .../domain/TicketSlotDTO.java | 7 + .../mapper/SchedulePoolMapper.java | 17 + .../mapper/ScheduleSlotMapper.java | 11 + .../com/openhis/clinical/domain/Order.java | 2 +- .../openhis/clinical/mapper/OrderMapper.java | 10 + .../service/impl/OrderServiceImpl.java | 1 + .../service/impl/TicketServiceImpl.java | 27 +- .../administration/ScheduleSlotMapper.xml | 161 ++++++++-- .../resources/mapper/clinical/OrderMapper.xml | 8 +- openhis-ui-vue3/src/utils/medicalConstants.js | 58 ++++ .../outpatientAppointment/index.vue | 39 ++- .../charge/outpatientregistration/index.vue | 293 +++++++++++++++++- 17 files changed, 821 insertions(+), 77 deletions(-) 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; } } From f6b39a48157a294da29123b9eeae1ab4f268a054 Mon Sep 17 00:00:00 2001 From: yangkexiang <1677036288@qq.com> Date: Fri, 3 Apr 2026 16:35:21 +0800 Subject: [PATCH 03/13] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0=E9=97=A8?= =?UTF-8?q?=E8=AF=8A=E5=AE=9A=E4=BB=B7=E6=9C=8D=E5=8A=A1=E4=BB=A5=E4=BB=85?= =?UTF-8?q?=E8=BF=94=E5=9B=9E=E5=88=92=E4=BB=B7=E6=A0=87=E8=AE=B0=E4=B8=BA?= =?UTF-8?q?=E2=80=9C=E6=98=AF=E2=80=9D=E7=9A=84=E9=A1=B9=E7=9B=AE=EF=BC=8C?= =?UTF-8?q?=E5=B9=B6=E4=BF=AE=E6=AD=A3=E6=97=A5=E5=BF=97=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E5=92=8CVitalSigns=E8=A1=A8=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改 OutpatientPricingAppServiceImpl.java,确保仅返回划价标记为“是”的项目 - 修正 VitalSigns.java 中的表名为 "doc_vital_signs" --- .../appservice/impl/OutpatientPricingAppServiceImpl.java | 5 ++--- .../main/java/com/openhis/document/domain/VitalSigns.java | 2 +- .../views/appoinmentmanage/outpatientAppointment/index.vue | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/chargemanage/appservice/impl/OutpatientPricingAppServiceImpl.java b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/chargemanage/appservice/impl/OutpatientPricingAppServiceImpl.java index dd32d54d..90c4d116 100644 --- a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/chargemanage/appservice/impl/OutpatientPricingAppServiceImpl.java +++ b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/chargemanage/appservice/impl/OutpatientPricingAppServiceImpl.java @@ -73,11 +73,10 @@ public class OutpatientPricingAppServiceImpl implements IOutpatientPricingAppSer } else { adviceTypes = List.of(1, 2, 3); } - // 门诊划价:不要强制 pricingFlag=1 参与过滤(wor_activity_definition.pricing_flag 可能为 0), - // 否则会导致诊疗项目(adviceType=3)查询结果为空 records=[] String categoryCode = adviceBaseDto != null ? adviceBaseDto.getCategoryCode() : null; + // 门诊划价:仅返回划价标记为“是”的项目 return iDoctorStationAdviceAppService.getAdviceBaseInfo(adviceBaseDto, searchKey, locationId, null, - organizationId, pageNo, pageSize, null, adviceTypes, null, categoryCode); + organizationId, pageNo, pageSize, Whether.YES.getValue(), adviceTypes, null, categoryCode); } } diff --git a/openhis-server-new/openhis-domain/src/main/java/com/openhis/document/domain/VitalSigns.java b/openhis-server-new/openhis-domain/src/main/java/com/openhis/document/domain/VitalSigns.java index 953513ed..d6160a97 100644 --- a/openhis-server-new/openhis-domain/src/main/java/com/openhis/document/domain/VitalSigns.java +++ b/openhis-server-new/openhis-domain/src/main/java/com/openhis/document/domain/VitalSigns.java @@ -17,7 +17,7 @@ import java.util.Date; * @date 2025-06-03 */ @Data -@TableName("doc_ital_signs") +@TableName("doc_vital_signs") @Accessors(chain = true) @EqualsAndHashCode(callSuper = false) public class VitalSigns extends HisBaseEntity { diff --git a/openhis-ui-vue3/src/views/appoinmentmanage/outpatientAppointment/index.vue b/openhis-ui-vue3/src/views/appoinmentmanage/outpatientAppointment/index.vue index aa36accd..5635bbfc 100644 --- a/openhis-ui-vue3/src/views/appoinmentmanage/outpatientAppointment/index.vue +++ b/openhis-ui-vue3/src/views/appoinmentmanage/outpatientAppointment/index.vue @@ -195,7 +195,7 @@ > {{ index + 1 }} {{ patient.name }} - {{ patient.id || patient.medicalCard }} + {{ patient.identifierNo }} {{ getGenderText(patient.genderEnum_enumText || patient.genderEnum || patient.gender || patient.sex) }} {{ patient.idCard }} {{ patient.phone }} From 8a84b40ee55e9ca92fe00b65c2277f57bcffe078 Mon Sep 17 00:00:00 2001 From: Ranyunqiao <2499115710@qq.com> Date: Fri, 3 Apr 2026 16:42:10 +0800 Subject: [PATCH 04/13] =?UTF-8?q?333=20=E9=97=A8=E8=AF=8A=E5=8C=BB?= =?UTF-8?q?=E7=94=9F=E7=AB=99=E5=BC=80=E7=AB=8B=E8=80=97=E6=9D=90=E5=8C=BB?= =?UTF-8?q?=E5=98=B1=E6=97=B6=EF=BC=8C=E7=B1=BB=E5=9E=8B=E8=AF=AF=E8=BD=AC?= =?UTF-8?q?=E4=B8=BA=E2=80=9C=E4=B8=AD=E6=88=90=E8=8D=AF=E2=80=9D=E4=B8=94?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DoctorStationAdviceAppServiceImpl.java | 41 ++++ .../web/doctorstation/utils/AdviceUtils.java | 9 + .../components/adviceBaseList.vue | 10 +- .../prescription/prescriptionlist.vue | 183 +++++++++++++----- 4 files changed, 194 insertions(+), 49 deletions(-) diff --git a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/doctorstation/appservice/impl/DoctorStationAdviceAppServiceImpl.java b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/doctorstation/appservice/impl/DoctorStationAdviceAppServiceImpl.java index 192bf778..8c427ce6 100644 --- a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/doctorstation/appservice/impl/DoctorStationAdviceAppServiceImpl.java +++ b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/doctorstation/appservice/impl/DoctorStationAdviceAppServiceImpl.java @@ -1201,6 +1201,47 @@ public class DoctorStationAdviceAppServiceImpl implements IDoctorStationAdviceAp chargeItem.setServiceId(deviceRequest.getId()); // 医疗服务ID chargeItem.setProductTable(adviceSaveDto.getAdviceTableName());// 产品所在表 chargeItem.setProductId(adviceSaveDto.getAdviceDefinitionId());// 收费项id + + // 🔧 Bug Fix: 如果 definitionId 或 definitionDetailId 为 null,从定价信息中获取 + if (chargeItem.getDefinitionId() == null || chargeItem.getDefDetailId() == null) { + log.warn("耗材的 definitionId 或 definitionDetailId 为 null,尝试从定价信息中获取: deviceDefId={}", + adviceSaveDto.getAdviceDefinitionId()); + // 查询耗材定价信息 + IPage devicePage = doctorStationAdviceAppMapper.getAdviceBaseInfo( + new Page<>(1, 1), + PublicationStatus.ACTIVE.getValue(), + orgId, + CommonConstants.TableName.ADM_DEVICE_DEFINITION, + null, + null, + null, + Arrays.asList(adviceSaveDto.getAdviceDefinitionId()), + null, + null, + null, + null); + if (devicePage != null && !devicePage.getRecords().isEmpty()) { + AdviceBaseDto deviceBaseInfo = devicePage.getRecords().get(0); + if (deviceBaseInfo.getPriceList() != null && !deviceBaseInfo.getPriceList().isEmpty()) { + AdvicePriceDto devicePrice = deviceBaseInfo.getPriceList().get(0); + if (chargeItem.getDefinitionId() == null) { + chargeItem.setDefinitionId(devicePrice.getDefinitionId()); + log.info("从定价信息中获取 definitionId: {}", devicePrice.getDefinitionId()); + } + if (chargeItem.getDefDetailId() == null) { + chargeItem.setDefDetailId(devicePrice.getDefinitionDetailId()); + log.info("从定价信息中获取 definitionDetailId: {}", devicePrice.getDefinitionDetailId()); + } + } + } + } + + // 🔧 Bug Fix: 确保定义ID不为null + if (chargeItem.getDefinitionId() == null) { + log.error("无法获取耗材的 definitionId: deviceDefId={}", adviceSaveDto.getAdviceDefinitionId()); + throw new ServiceException("无法获取耗材的定价信息,请联系管理员"); + } + // 🔧 Bug Fix: 如果accountId为null,从就诊中获取账户ID,如果没有则自动创建 Long accountId = adviceSaveDto.getAccountId(); if (accountId == null) { diff --git a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/doctorstation/utils/AdviceUtils.java b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/doctorstation/utils/AdviceUtils.java index 3278124b..5e4e9c28 100644 --- a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/doctorstation/utils/AdviceUtils.java +++ b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/doctorstation/utils/AdviceUtils.java @@ -115,6 +115,15 @@ public class AdviceUtils { matched = true; // 检查库存是否充足 BigDecimal minUnitQuantity = saveDto.getMinUnitQuantity(); + // 🔧 Bug Fix: 对于耗材类型,如果没有设置minUnitQuantity,则使用quantity作为默认值 + if (minUnitQuantity == null) { + if (CommonConstants.TableName.ADM_DEVICE_DEFINITION.equals(inventoryDto.getItemTable())) { + // 耗材只有一个单位,minUnitQuantity等于quantity + minUnitQuantity = saveDto.getQuantity(); + } else { + return saveDto.getAdviceName() + "的小单位数量不能为空"; + } + } BigDecimal chineseHerbsDoseQuantity = saveDto.getChineseHerbsDoseQuantity(); // 中药付数 // 中草药医嘱的情况 if (chineseHerbsDoseQuantity != null && chineseHerbsDoseQuantity.compareTo(BigDecimal.ZERO) > 0) { diff --git a/openhis-ui-vue3/src/views/doctorstation/components/adviceBaseList.vue b/openhis-ui-vue3/src/views/doctorstation/components/adviceBaseList.vue index e8516337..7a994694 100644 --- a/openhis-ui-vue3/src/views/doctorstation/components/adviceBaseList.vue +++ b/openhis-ui-vue3/src/views/doctorstation/components/adviceBaseList.vue @@ -273,7 +273,10 @@ function fetchFromApi(searchKey) { minUnitCode_dictText: item.minUnitCode_dictText || item.unitCode_dictText || '', volume: item.size || item.totalVolume || '', partPercent: item.partPercent || 1, - inventoryList: [], + // 🔧 Bug Fix: 如果后端提供了inventoryList,则使用;否则为空数组 + inventoryList: item.inventoryList || [], + // 🔧 Bug Fix: 构造stockList用于库存显示 + stockList: item.inventoryList || [], adviceDefinitionId: item.id, chargeItemDefinitionId: item.id, positionId: item.locationId, @@ -354,6 +357,11 @@ function handleQuantity(row) { const totalQuantity = row.inventoryList.reduce((sum, item) => sum + (item.quantity || 0), 0); return totalQuantity.toString() + row.minUnitCode_dictText; } + // 🔧 Bug Fix: 耗材类型可能没有inventoryList,但可能有stockList + if (row.stockList && row.stockList.length > 0) { + const totalQuantity = row.stockList.reduce((sum, item) => sum + (item.quantity || 0), 0); + return totalQuantity.toString() + row.minUnitCode_dictText; + } return 0; } diff --git a/openhis-ui-vue3/src/views/doctorstation/components/prescription/prescriptionlist.vue b/openhis-ui-vue3/src/views/doctorstation/components/prescription/prescriptionlist.vue index 9f51baf6..d4e26d0f 100644 --- a/openhis-ui-vue3/src/views/doctorstation/components/prescription/prescriptionlist.vue +++ b/openhis-ui-vue3/src/views/doctorstation/components/prescription/prescriptionlist.vue @@ -546,6 +546,11 @@ expandOrder = []; // 当医嘱类型改变时,清空当前选择的项目名称,因为不同类型项目的数据结构可能不兼容 prescriptionList[scope.$index].adviceName = undefined; + prescriptionList[scope.$index].adviceType_dictText = ''; + // 🔧 Bug Fix: 医嘱类型改变时,重置minUnitQuantity和minUnitCode,避免null值 + prescriptionList[scope.$index].minUnitQuantity = prescriptionList[scope.$index].quantity || 1; + prescriptionList[scope.$index].minUnitCode = prescriptionList[scope.$index].unitCode; + prescriptionList[scope.$index].minUnitCode_dictText = prescriptionList[scope.$index].unitCode_dictText; adviceQueryParams.adviceTypes = value; // 🎯 修复:改为 adviceTypes(复数) // 根据选择的类型设置categoryCode,用于药品分类筛选 @@ -956,25 +961,38 @@ const { method_code, unit_code, rate_code, distribution_category_code, drord_doc // drord_doctor_type: 1=西药, 2=中成药, 3=诊疗, 4=耗材, 5=会诊, 6=手术 const adviceTypeList = computed(() => { // 如果字典已加载,使用字典数据;否则使用默认值 + let list = []; if (drord_doctor_type.value && drord_doctor_type.value.length > 0) { - return drord_doctor_type.value.map(item => ({ - label: item.label, - value: parseInt(item.value) || item.value - })); + // 过滤掉字典中已有的"全部"选项,避免重复 + list = drord_doctor_type.value + .filter(item => item.label !== '全部') + .map(item => ({ + label: item.label, + value: parseInt(item.value) || item.value + })); + } else { + // 默认返回值,确保页面正常显示 + list = [ + { label: '西药', value: 1 }, + { label: '中成药', value: 2 }, + { label: '诊疗', value: 3 }, + { label: '耗材', value: 4 }, + { label: '会诊', value: 5 }, + { label: '手术', value: 6 }, + ]; } - // 默认返回值,确保页面正常显示 - return [ - { label: '西药', value: 1 }, - { label: '中成药', value: 2 }, - { label: '诊疗', value: 3 }, - { label: '耗材', value: 4 }, - { label: '会诊', value: 5 }, - { label: '手术', value: 6 }, - ]; + // 在最后添加"全部"选项 + list.push({ label: '全部', value: 0 }); + return list; }); // 根据类型值获取显示标签,避免非编辑态出现空标签 -const mapAdviceTypeLabel = (type) => { +const mapAdviceTypeLabel = (type, adviceTableName) => { + // 🔧 Bug Fix: 根据adviceTableName判断耗材类型 + // 后端adviceType=2既表示中成药又表示耗材,需要通过表名区分 + if (type === 2 && adviceTableName === 'adm_device_definition') { + return '耗材'; + } const found = adviceTypeList.value.find((item) => item.value === type); return found ? found.label : ''; }; @@ -1602,11 +1620,8 @@ function getListInfo(addNewRow) { // 🔧 Bug Fix: 后端保存时将耗材(4)转换为中成药(2),显示时需要转换回来 // 检查 adviceTableName,如果是耗材表则应该是耗材类型 const adviceTableName = contentJson?.adviceTableName || item.adviceTableName; - if (adviceType === 2 && adviceTableName === 'adm_device_definition') { - adviceType = 4; // 后端2(中成药) -> 前端4(耗材) - } - let adviceType_dictText = item.adviceType_dictText || mapAdviceTypeLabel(adviceType); + let adviceType_dictText = item.adviceType_dictText || mapAdviceTypeLabel(adviceType, adviceTableName); // 如果是会诊类型,设置为会诊类型 if (isConsultation) { @@ -2343,6 +2358,13 @@ function handleSave(prescriptionId) { item.accountId = finalAccountId; } item.dbOpType = '1'; + + // 🔧 Bug Fix: 确保耗材的minUnitQuantity被正确设置 + if (item.adviceType == 4) { + item.minUnitQuantity = item.quantity; + item.minUnitCode = item.unitCode; + item.minUnitCode_dictText = item.unitCode_dictText; + } }); loading.value = true; @@ -2366,20 +2388,25 @@ function handleSave(prescriptionId) { finalUnitCode = item.minUnitCode; } item.minUnitQuantity = finalQuantity; + } else if (item.adviceType == 4) { + // 🔧 Bug Fix: 耗材类型只有一个单位,minUnitQuantity等于quantity + item.minUnitQuantity = item.quantity; + // 🔧 Bug Fix: 确保minUnitCode等于unitCode + item.minUnitCode = item.unitCode; + item.minUnitCode_dictText = item.unitCode_dictText; + finalUnitCode = item.unitCode; } else { item.minUnitQuantity = item.quantity; } // --- 【修改点3:Bug 1 修复 (类型转换)】 --- let saveAdviceType = item.adviceType; - if (item.adviceType == 4) { - saveAdviceType = 2; // 耗材:前端4 -> 后端2 - } else if (item.adviceType == 2) { - saveAdviceType = 1; // 中成药:前端2 -> 后端1 - } else if (item.adviceType == 5) { + // 🔧 Bug Fix: 保持原类型,不进行转换 + // 前端类型: 1=西药, 2=中成药, 3=诊疗, 4=耗材, 5=会诊, 6=手术 + // 后端类型: 1=药品, 2=耗材, 3=医疗活动, 6=手术 + // 后端会通过 ItemType.DEVICE.getValue() || adviceType == 4 来识别耗材 + if (item.adviceType == 5) { saveAdviceType = 3; // 会诊:前端5 -> 后端3(诊疗类) - } else if (item.adviceType == 6) { - saveAdviceType = 6; // 🔧 BugFix#318: 手术类型保持为6 } // 🔧 Bug Fix: Validate and fix NaN values before sending to backend @@ -2439,6 +2466,8 @@ function handleSave(prescriptionId) { quantity: finalQuantity, unitCode: finalUnitCode, totalPrice: item.totalPrice, // Ensure totalPrice is valid + // 🔧 Bug Fix: 确保 categoryEnum 被传递(耗材必填字段) + categoryEnum: item.categoryEnum || parsedContent.categoryEnum, // 🔧 Bug Fix: 确保库存匹配成功的关键字段 adviceTableName: adviceTableNameVal, locationId: locationIdVal, @@ -2446,6 +2475,13 @@ function handleSave(prescriptionId) { methodCode: item.methodCode || parsedContent.methodCode, // 🔧 确保 accountId 被传递(用于预结算) accountId: item.accountId || parsedContent.accountId, + // 🔧 Bug Fix: 确保minUnitQuantity被传递(耗材必填字段) + minUnitQuantity: item.minUnitQuantity, + minUnitCode: item.minUnitCode, + minUnitCode_dictText: item.minUnitCode_dictText, + // 🔧 Bug Fix: 确保 definitionId 和 definitionDetailId 被传递(费用项必填字段) + definitionId: item.definitionId || parsedContent.definitionId, + definitionDetailId: item.definitionDetailId || parsedContent.definitionDetailId, // 🔧 更新 contentJson 中的 adviceType,确保后端分类正确 contentJson: JSON.stringify({ ...parsedContent, @@ -2635,12 +2671,17 @@ function handleOrderBindInfo(bindIdInfo, currentMethodCode) { encounterDiagnosisId: encounterDiagnosisId.value, // 🔧 确保 adviceType 和显示文本正确设置 adviceType: item.adviceType, - adviceType_dictText: mapAdviceTypeLabel(item.adviceType), + adviceType_dictText: mapAdviceTypeLabel(item.adviceType, item.adviceTableName), }; // 计算价格和总量 const unitInfo = unitCodeList.value.find((k) => k.value == item.unitCode); - if (unitInfo && unitInfo.type == 'minUnit') { + // 🔧 Bug Fix: 耗材类型只有一个单位,minUnitQuantity等于quantity + if (item.adviceType == 4) { + newRow.price = newRow.unitPrice; + newRow.totalPrice = (item.quantity * newRow.unitPrice).toFixed(6); + newRow.minUnitQuantity = item.quantity; + } else if (unitInfo && unitInfo.type == 'minUnit') { newRow.price = newRow.minUnitPrice; newRow.totalPrice = (item.quantity * newRow.minUnitPrice).toFixed(6); newRow.minUnitQuantity = item.quantity; @@ -2705,6 +2746,12 @@ function handleSaveSign(row, index, prescriptionId) { return; } + // 🔧 Bug Fix: 验证医嘱类型不能为"全部" + if (row.adviceType === 0) { + proxy.$modal.msgWarning('请选择医嘱类型'); + return; + } + // 重新查找索引,确保使用当前处方列表中的正确索引 const actualIndex = prescriptionList.value.findIndex(item => item.uniqueKey === row.uniqueKey); if (actualIndex === -1) { @@ -2774,14 +2821,18 @@ function handleSaveSign(row, index, prescriptionId) { row.encounterId = props.patientInfo.encounterId; row.accountId = accountId.value; // 确保非编辑态显示正确的医嘱类型标签 - row.adviceType_dictText = mapAdviceTypeLabel(row.adviceType); + row.adviceType_dictText = mapAdviceTypeLabel(row.adviceType, row.adviceTableName); // 更新本地显示的最小单位数量(仅用于前端逻辑,不影响显示单位) if (row.adviceType == 1 || row.adviceType == 2) { row.minUnitQuantity = row.minUnitCode == row.unitCode ? row.quantity : row.quantity * row.partPercent; } else { + // 🔧 Bug Fix: 耗材和其他类型,minUnitQuantity等于quantity row.minUnitQuantity = row.quantity; + // 🔧 Bug Fix: 确保minUnitCode等于unitCode(耗材只有一个单位) + row.minUnitCode = row.unitCode; + row.minUnitCode_dictText = row.unitCode_dictText; } row.conditionId = conditionId.value; @@ -2976,14 +3027,12 @@ function handleSaveBatch(prescriptionId) { // --- Bug 1 修复:类型转换 --- let saveAdviceType = item.adviceType; - if (item.adviceType == 4) { - saveAdviceType = 2; // 耗材前端4 -> 后端2 - } else if (item.adviceType == 2) { - saveAdviceType = 1; // 中成药前端2 -> 后端1 - } else if (item.adviceType == 5) { + // 🔧 Bug Fix: 保持原类型,不进行转换 + // 前端类型: 1=西药, 2=中成药, 3=诊疗, 4=耗材, 5=会诊, 6=手术 + // 后端类型: 1=药品, 2=耗材, 3=医疗活动, 6=手术 + // 后端会通过 ItemType.DEVICE.getValue() || adviceType == 4 来识别耗材 + if (item.adviceType == 5) { saveAdviceType = 3; // 会诊前端5 -> 后端3(诊疗类) - } else if (item.adviceType == 6) { - saveAdviceType = 6; // 🔧 BugFix#318: 手术类型保持为6 } // 🔧 BugFix#318: 过滤掉手术特有字段,只保留标准医嘱字段 @@ -2994,7 +3043,11 @@ function handleSaveBatch(prescriptionId) { 'encounterId', 'groupId', 'injectFlag', 'lotNumber', 'methodCode', 'partPercent', 'patientId', 'positionId', 'positionName', 'prescriptionNo', 'quantity', 'rateCode', 'requestId', 'skinTestFlag', 'sortNumber', 'statusEnum', 'totalPrice', - 'unitCode', 'unitPrice', 'volume', 'ybClassEnum' + 'unitCode', 'unitPrice', 'volume', 'ybClassEnum', + // 🔧 Bug Fix: 添加 definitionId 和 definitionDetailId 字段 + 'definitionId', 'definitionDetailId', + // 🔧 Bug Fix: 添加 categoryEnum 字段(耗材必填) + 'categoryEnum' ]; let filteredItem = {}; standardItemFields.forEach(field => { @@ -3193,17 +3246,26 @@ function syncGroupFields(row) { function setValue(row) { unitCodeList.value = []; unitCodeList.value.push({ value: row.unitCode, label: row.unitCode_dictText, type: 'unit' }); - unitCodeList.value.push({ - value: row.doseUnitCode, - label: row.doseUnitCode_dictText, - type: 'dose', - }); + + // 🔧 Bug Fix: 耗材类型只有一个单位,不需要dose和minUnit选项 + if (row.adviceType == 4) { + // 耗材只添加一个单位选项 + row.minUnitCode = row.unitCode; + row.minUnitCode_dictText = row.unitCode_dictText; + } else { + // 药品类型添加dose和minUnit选项 + unitCodeList.value.push({ + value: row.doseUnitCode, + label: row.doseUnitCode_dictText, + type: 'dose', + }); - unitCodeList.value.push({ - value: row.minUnitCode, - label: row.minUnitCode_dictText, - type: 'minUnit', - }); + unitCodeList.value.push({ + value: row.minUnitCode, + label: row.minUnitCode_dictText, + type: 'minUnit', + }); + } if (row.adviceType == 2 && row.minUnitCode != row.unitCode) { unitCodeList.value.push({ value: row.minUnitCode, @@ -3237,8 +3299,15 @@ function setValue(row) { prescriptionList.value[rowIndex.value].unitCodeList = unitCodeList.value; prescriptionList.value[rowIndex.value].doseUnitCode = row.doseUnitCode; prescriptionList.value[rowIndex.value].minUnitCode = row.minUnitCode; - prescriptionList.value[rowIndex.value].unitCode = - row.partAttributeEnum == 1 ? row.minUnitCode : row.unitCode; + // 🔧 Bug Fix: 耗材类型只有一个单位,minUnitCode应该等于unitCode + if (Number(row.adviceType) == 4) { + prescriptionList.value[rowIndex.value].minUnitCode = row.unitCode; + prescriptionList.value[rowIndex.value].minUnitCode_dictText = row.unitCode_dictText; + prescriptionList.value[rowIndex.value].unitCode = row.unitCode; + } else { + prescriptionList.value[rowIndex.value].unitCode = + row.partAttributeEnum == 1 ? row.minUnitCode : row.unitCode; + } prescriptionList.value[rowIndex.value].categoryEnum = row.categoryCode; prescriptionList.value[rowIndex.value].skinTestFlag = row.skinTestFlag; prescriptionList.value[rowIndex.value].definitionId = row.chargeItemDefinitionId; @@ -3307,6 +3376,10 @@ function setValue(row) { // 🔧 Bug #218 修复:保留组套中的quantity,如果没有则默认1 prescriptionList.value[rowIndex.value].quantity = row.quantity || 1; prescriptionList.value[rowIndex.value].totalPrice = validPrice * (row.quantity || 1); + // 🔧 Bug Fix: 设置耗材的minUnitQuantity,避免保存时null错误 + prescriptionList.value[rowIndex.value].minUnitQuantity = row.quantity || 1; + // 🔧 Bug Fix: 设置耗材的categoryEnum,避免数据库约束错误 + prescriptionList.value[rowIndex.value].categoryEnum = row.categoryCode || 3; // 默认为3 prescriptionList.value[rowIndex.value].positionName = row.positionName || ''; // 🔧 Bug Fix: 使用 positionId,如果为空则使用患者信息中的 orgId console.log('设置耗材locationId:', { @@ -3325,6 +3398,10 @@ function setValue(row) { prescriptionList.value[rowIndex.value].minUnitPrice = 0; prescriptionList.value[rowIndex.value].quantity = row.quantity || 1; prescriptionList.value[rowIndex.value].totalPrice = 0; + // 🔧 Bug Fix: 设置耗材的minUnitQuantity,避免保存时null错误 + prescriptionList.value[rowIndex.value].minUnitQuantity = row.quantity || 1; + // 🔧 Bug Fix: 设置耗材的categoryEnum,避免数据库约束错误 + prescriptionList.value[rowIndex.value].categoryEnum = row.categoryCode || 3; // 默认为3 prescriptionList.value[rowIndex.value].positionName = row.positionName || ''; const finalLocationId = row.positionId || props.patientInfo.orgId; prescriptionList.value[rowIndex.value].locationId = finalLocationId; @@ -3967,6 +4044,16 @@ function convertDoseValues(row, index) { // 总量计算,仅适用只有两种单位的情况 function calculateTotalAmount(row, index) { nextTick(() => { + // 🔧 Bug Fix: 处理耗材类型的总金额计算 + if (row.adviceType == 4) { + row.totalPrice = (row.quantity * row.unitPrice).toFixed(6); + // 🔧 Bug Fix: 确保耗材的minUnitQuantity和minUnitCode正确设置 + row.minUnitQuantity = row.quantity; + row.minUnitCode = row.unitCode; + row.minUnitCode_dictText = row.unitCode_dictText; + return; + } + if (row.adviceType == 2) { calculateTotalPrice(row, index); return; From 0c5353cf8b9ce89524b19e70087834b3f703daf7 Mon Sep 17 00:00:00 2001 From: HuangXinQuan Date: Fri, 3 Apr 2026 16:47:03 +0800 Subject: [PATCH 05/13] =?UTF-8?q?300=EF=BC=8C301=EF=BC=8C302=E9=A2=84?= =?UTF-8?q?=E7=BA=A6=E6=8C=82=E5=8F=B7=E5=B1=95=E7=A4=BA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../appservice/impl/TicketAppServiceImpl.java | 2 + .../web/appointmentmanage/dto/TicketDto.java | 5 + .../domain/TicketSlotDTO.java | 1 + .../administration/ScheduleSlotMapper.xml | 3 + .../outpatientAppointment/index.vue | 46 +++++-- .../examination/examinationApplication.vue | 117 ++++++++++++++++-- 6 files changed, 152 insertions(+), 22 deletions(-) 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 f63290d2..2cff7c07 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 @@ -142,6 +142,7 @@ public class TicketAppServiceImpl implements ITicketAppService { // 基础字段映射 dto.setSlot_id(raw.getSlotId()); + dto.setSeqNo(raw.getSeqNo()); dto.setBusNo(String.valueOf(raw.getSlotId())); dto.setDoctor(raw.getDoctor()); dto.setDepartment(raw.getDepartmentName()); // 注意:以前这里传成了ID,导致前端出Bug,现在修复成了真正的科室名 @@ -319,6 +320,7 @@ public class TicketAppServiceImpl implements ITicketAppService { // --- 基础字段处理 --- // 注意:这里已经变成了极其舒服的 .getSlotId() 方法调用,告别魔鬼字符串! dto.setSlot_id(raw.getSlotId()); + dto.setSeqNo(raw.getSeqNo()); dto.setBusNo(String.valueOf(raw.getSlotId())); // 暂时借用真实槽位ID做唯一流水号 dto.setDoctor(raw.getDoctor()); dto.setDepartment(raw.getDepartmentName()); 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 74e94643..afc073a4 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 @@ -23,6 +23,11 @@ public class TicketDto { @JsonSerialize(using = ToStringSerializer.class) private Long slot_id; + /** + * 号源序号(对应 adm_schedule_slot.seq_no) + */ + private Integer seqNo; + /** * 号源编码 */ 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 351d1015..fa778a91 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 @@ -11,6 +11,7 @@ import java.time.LocalTime; public class TicketSlotDTO { // 基础信息 private Long slotId; + private Integer seqNo; private Long scheduleId; private String doctor; private Long doctorId; 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 7f50bfe3..f93a7e6c 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 @@ -43,6 +43,7 @@ SELECT s.id AS slotId, + s.seq_no AS seqNo, p.schedule_id AS scheduleId, p.doctor_name AS doctor, p.doctor_id AS doctorId, @@ -205,6 +207,7 @@