diff --git a/.qwen/settings.json b/.qwen/settings.json index 7d70b758..1287d769 100644 --- a/.qwen/settings.json +++ b/.qwen/settings.json @@ -2,5 +2,5 @@ "tools": { "approvalMode": "yolo" }, - "$version": 2 + "$version": 3 } \ No newline at end of file diff --git a/.qwen/settings.json.orig b/.qwen/settings.json.orig new file mode 100644 index 00000000..7d70b758 --- /dev/null +++ b/.qwen/settings.json.orig @@ -0,0 +1,6 @@ +{ + "tools": { + "approvalMode": "yolo" + }, + "$version": 2 +} \ No newline at end of file diff --git a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/appservice/ITicketAppService.java b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/appservice/ITicketAppService.java index e46c7083..9891c12e 100644 --- a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/appservice/ITicketAppService.java +++ b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/appservice/ITicketAppService.java @@ -2,7 +2,9 @@ package com.openhis.web.appointmentmanage.appservice; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.core.common.core.domain.R; +import com.openhis.appointmentmanage.dto.TicketQueryDTO; import com.openhis.web.appointmentmanage.dto.TicketDto; +import com.openhis.appointmentmanage.dto.TicketQueryDTO; import java.util.Map; @@ -14,37 +16,53 @@ import java.util.Map; public interface ITicketAppService { /** - * 预约号源 + * 分页查询门诊号源列表(真分页) * - * @param params 预约参数 + * @param query 查询参数 * @return 结果 */ - R bookTicket(Map params); + R listTicket(TicketQueryDTO query); + + /** + * 查询医生余号汇总(基于号源池,不受分页影响) + * + * @param query 查询参数 + * @return 结果 + */ + R listDoctorAvailability(TicketQueryDTO query); + + /** + * 预约号源 + * + * @param dto 预约参数 + * @return 结果 + */ + R bookTicket(com.openhis.appointmentmanage.domain.AppointmentBookDTO dto); /** * 取消预约 * - * @param ticketId 号源ID + * @param slotId 槽位ID * @return 结果 */ - R cancelTicket(Long ticketId); + R cancelTicket(Long slotId); /** * 取号 * - * @param ticketId 号源ID + * @param slotId 槽位ID * @return 结果 */ - R checkInTicket(Long ticketId); + R checkInTicket(Long slotId); /** * 停诊 * - * @param ticketId 号源ID + * @param slotId 槽位ID * @return 结果 */ - R cancelConsultation(Long ticketId); - + R cancelConsultation(Long slotId); + /** * 查询所有号源(用于测试) * diff --git a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/appservice/impl/DoctorScheduleAppServiceImpl.java b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/appservice/impl/DoctorScheduleAppServiceImpl.java index eaf514ef..d9631ac1 100644 --- a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/appservice/impl/DoctorScheduleAppServiceImpl.java +++ b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/appservice/impl/DoctorScheduleAppServiceImpl.java @@ -274,9 +274,9 @@ public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService { doctorSchedule.getLimitNumber()) .set(doctorSchedule.getStopReason() != null, SchedulePool::getStopReason, doctorSchedule.getStopReason()) .set(doctorSchedule.getRegType() != null, SchedulePool::getRegType, String.valueOf(doctorSchedule.getRegType())) - .set(doctorSchedule.getRegisterFee() != null, SchedulePool::getFee, doctorSchedule.getRegisterFee() / 100.0) + .set(doctorSchedule.getRegisterFee() != null, SchedulePool::getFee, Double.valueOf(doctorSchedule.getRegisterFee().toString())) .set(doctorSchedule.getRegisterFee() != null, SchedulePool::getInsurancePrice, - doctorSchedule.getRegisterFee() / 100.0) + Double.valueOf(doctorSchedule.getRegisterFee().toString())) .update(); } @@ -306,7 +306,7 @@ public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService { // 不设置available_num,因为它是数据库生成列 // pool.setAvailableNum(0); // 初始为0,稍后更新 pool.setRegType(schedule.getRegisterItem() != null ? schedule.getRegisterItem() : "普通"); - pool.setFee(schedule.getRegisterFee() != null ? schedule.getRegisterFee() / 100.0 : 0.0); // 假设数据库中以分为单位存储 + pool.setFee(schedule.getRegisterFee() != null ? Double.valueOf(schedule.getRegisterFee().toString()) : 0.0); // 直接使用原始价格 pool.setInsurancePrice(pool.getFee()); // 医保价格暂时与原价相同 // 暂时设置support_channel为空字符串,避免JSON类型问题 pool.setSupportChannel(""); @@ -359,7 +359,7 @@ public class DoctorScheduleAppServiceImpl implements IDoctorScheduleAppService { // 不设置available_num,因为它是数据库生成列 // pool.setAvailableNum(0); // 初始为0,稍后更新 pool.setRegType(schedule.getRegisterItem() != null ? schedule.getRegisterItem() : "普通"); - pool.setFee(schedule.getRegisterFee() != null ? schedule.getRegisterFee() / 100.0 : 0.0); // 假设数据库中以分为单位存储 + pool.setFee(schedule.getRegisterFee() != null ? Double.valueOf(schedule.getRegisterFee().toString()) : 0.0); // 直接使用原始价格 pool.setInsurancePrice(pool.getFee()); // 医保价格暂时与原价相同 // 暂时设置support_channel为空字符串,避免JSON类型问题 pool.setSupportChannel(""); 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 7c00ceec..573285cd 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 @@ -4,28 +4,21 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.core.common.core.domain.R; import com.openhis.administration.domain.Patient; import com.openhis.administration.service.IPatientService; -import com.openhis.appointmentmanage.domain.DoctorSchedule; -import com.openhis.appointmentmanage.mapper.DoctorScheduleMapper; -import com.openhis.appointmentmanage.service.IDoctorScheduleService; -import com.openhis.clinical.domain.Order; +import com.openhis.appointmentmanage.mapper.ScheduleSlotMapper; import com.openhis.clinical.domain.Ticket; -import com.openhis.clinical.mapper.OrderMapper; import com.openhis.clinical.service.ITicketService; -import com.openhis.web.appointmentmanage.appservice.IDoctorScheduleAppService; import com.openhis.web.appointmentmanage.appservice.ITicketAppService; import com.openhis.web.appointmentmanage.dto.TicketDto; +import com.openhis.common.constant.CommonConstants.SlotStatus; +import com.openhis.common.constant.CommonConstants.AppointmentOrderStatus; import org.springframework.stereotype.Service; import javax.annotation.Resource; -import java.text.SimpleDateFormat; -import java.time.*; import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; import java.util.List; -import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + /** * 号源管理应用服务实现类 * @@ -36,73 +29,41 @@ public class TicketAppServiceImpl implements ITicketAppService { @Resource private ITicketService ticketService; - + @Resource + private ScheduleSlotMapper scheduleSlotMapper; @Resource private IPatientService patientService; - @Resource - private IDoctorScheduleAppService doctorScheduleAppService; - @Resource - private DoctorScheduleMapper doctorScheduleMapper; - @Resource - private OrderMapper orderMapper; private static final Logger log = LoggerFactory.getLogger(TicketAppServiceImpl.class); /** - * 预约号源 + * 预约号源 (重构版:精准锁定单一槽位) * - * @param params 预约参数 + * @param dto 预约参数 * @return 结果 */ @Override - public R bookTicket(Map params) { - // 1. 获取 ticketId 和 slotId - Long ticketId = null; - Long slotId = null; - if (params.get("ticketId") != null) { - ticketId = Long.valueOf(params.get("ticketId").toString()); + public R bookTicket(com.openhis.appointmentmanage.domain.AppointmentBookDTO dto) { + Long slotId = dto.getSlotId(); + if (slotId == null) { + return R.fail("参数校验失败:缺少排班槽位唯一标识"); } - if (params.get("slotId") != null) { - slotId = Long.valueOf(params.get("slotId").toString()); - } - // 2. 参数校验 - if (ticketId == null || slotId == null) { - return R.fail("参数错误:ticketId 或 slotId 不能为空"); - } - try { - // 3. 执行原有的预约逻辑 - int result = ticketService.bookTicket(params); + int result = ticketService.bookTicket(dto); if (result > 0) { - // 4. 预约成功后,更新排班表状态 - DoctorSchedule schedule = new DoctorSchedule(); - schedule.setId(slotId); // 对应 XML 中的 WHERE id = #{id} - schedule.setIsStopped(true); // 设置为已预约 - schedule.setStopReason("booked"); // 设置停用原因 - - // 执行更新 - int updateCount = doctorScheduleMapper.updateDoctorSchedule(schedule); - - if (updateCount > 0) { - return R.ok("预约成功并已更新排班状态"); - } else { - // 如果更新失败,可能需要根据业务逻辑决定是否回滚预约 - return R.ok("预约成功,但排班状态更新失败"); - } - } else { - return R.fail("预约失败"); + return R.ok("预约成功!号源已安全锁定。"); } + return R.fail("预约挂单核发失败"); } catch (Exception e) { - // e.printStackTrace(); - log.error(e.getMessage()); + log.error("大厅挂号捕获系统异常", e); return R.fail("系统异常:" + e.getMessage()); } } /** - * 取消预约 + * 取消预约 (重构版:精准释放单一槽位) * - * @param slotId 医生排班ID + * @param slotId 医生槽位排班ID * @return 结果 */ @Override @@ -111,18 +72,8 @@ public class TicketAppServiceImpl implements ITicketAppService { return R.fail("参数错误"); } try { - ticketService.cancelTicket(slotId); - DoctorSchedule schedule = new DoctorSchedule(); - schedule.setId(slotId); // 对应 WHERE id = #{id} - schedule.setIsStopped(false); // 设置为 false - schedule.setStopReason(""); // 将原因清空 (设为空字符串) - // 3. 调用自定义更新方法 - int updateCount = doctorScheduleMapper.updateDoctorSchedule(schedule); - if (updateCount > 0) { - return R.ok("取消成功"); - } else { - return R.ok("取消成功"); - } + int result = ticketService.cancelTicket(slotId); + return R.ok(result > 0 ? "取消成功,号源已重新释放回市场" : "取消失败"); } catch (Exception e) { return R.fail(e.getMessage()); } @@ -131,16 +82,16 @@ public class TicketAppServiceImpl implements ITicketAppService { /** * 取号 * - * @param ticketId 号源ID + * @param slotId 槽位ID * @return 结果 */ @Override - public R checkInTicket(Long ticketId) { - if (ticketId == null) { + public R checkInTicket(Long slotId) { + if (slotId == null) { return R.fail("参数错误"); } try { - int result = ticketService.checkInTicket(ticketId); + int result = ticketService.checkInTicket(slotId); return R.ok(result > 0 ? "取号成功" : "取号失败"); } catch (Exception e) { return R.fail(e.getMessage()); @@ -150,109 +101,201 @@ public class TicketAppServiceImpl implements ITicketAppService { /** * 停诊 * - * @param ticketId 号源ID + * @param slotId 槽位ID * @return 结果 */ @Override - public R cancelConsultation(Long ticketId) { - if (ticketId == null) { + public R cancelConsultation(Long slotId) { + if (slotId == null) { return R.fail("参数错误"); } try { - int result = ticketService.cancelConsultation(ticketId); + int result = ticketService.cancelConsultation(slotId); return R.ok(result > 0 ? "停诊成功" : "停诊失败"); } catch (Exception e) { return R.fail(e.getMessage()); } } - + @Override - public R listAllTickets() { - // 1. 从 AppService 获取排班数据 - R response = doctorScheduleAppService.getDoctorScheduleList(); - // 获取返回的 List 数据 (假设 R.ok 里的数据是 List) - List scheduleList = (List) response.getData(); + public R listTicket(com.openhis.appointmentmanage.dto.TicketQueryDTO query) { + // 1. 防空指针处理 + if (query == null) { + query = new com.openhis.appointmentmanage.dto.TicketQueryDTO(); + } - // 2. 转换数据为 TicketDto - List tickets = new ArrayList<>(); + // 2. 构造 MyBatis 的分页对象 (传入前端给的当前页和每页条数) + com.baomidou.mybatisplus.extension.plugins.pagination.Page pageParam = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>( + query.getPage(), query.getLimit()); - if (scheduleList != null) { - for (DoctorSchedule schedule : scheduleList) { + // 3. 调用刚才写的底层动态 SQL 查询! + com.baomidou.mybatisplus.extension.plugins.pagination.Page rawPage = scheduleSlotMapper + .selectTicketSlotsPage(pageParam, query); + + // 4. 将查出来的数据翻译为前端可以直接渲染的结构 + java.util.List tickets = new java.util.ArrayList<>(); + if (rawPage.getRecords() != null) { + for (com.openhis.appointmentmanage.domain.TicketSlotDTO raw : rawPage.getRecords()) { TicketDto dto = new TicketDto(); - // 基础信息映射 - dto.setSlot_id(Long.valueOf(schedule.getId())); // Integer 转 Long - dto.setBusNo(String.valueOf(schedule.getId())); // 生成一个业务编号 - dto.setDepartment(String.valueOf(schedule.getDeptId())); // 如果有科室名建议关联查询,这里暂填ID - dto.setDoctor(schedule.getDoctor()); + // 基础字段映射 + dto.setSlot_id(raw.getSlotId()); + dto.setBusNo(String.valueOf(raw.getSlotId())); + dto.setDoctor(raw.getDoctor()); + 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.setPhone(raw.getPhone()); - // 号源类型处理:根据挂号项目判断是普通号还是专家号 - String registerItem = schedule.getRegisterItem(); - if (registerItem != null && registerItem.contains("专家")) { + // 号源类型处理 (底层是1,前端要的是expert) + if (raw.getRegType() != null && raw.getRegType() == 1) { dto.setTicketType("expert"); } else { dto.setTicketType("general"); } - // 时间处理:格式化为日期+时间范围,如 "2025-12-01 08:00-12:00" - String currentDate = LocalDate.now().toString(); // 或者从schedule中获取具体日期 - String timeRange = schedule.getStartTime() + "-" + schedule.getEndTime(); - dto.setDateTime(currentDate + " " + timeRange); - LocalTime nowTime = LocalTime.now(); - LocalTime endTime = schedule.getEndTime(); - String stopReason1 = schedule.getStopReason(); - if ("cancelled".equals(stopReason1)||(endTime != null && nowTime.isAfter(endTime))) { - dto.setStatus("已停诊"); - }else if (Boolean.TRUE.equals(schedule.getIsStopped())) { - // 获取原因并处理可能的空值 - String stopReason = schedule.getStopReason(); - // 使用 .equals() 比较内容,并将常量放在前面防止空指针 - if ("booked".equals(stopReason)) { - dto.setStatus("已预约"); - // --- 新增:获取患者信息 --- - List Order = orderMapper.selectOrderBySlotId(Long.valueOf(schedule.getId())); - Order latestOrder=Order.get(0); - if (latestOrder != null) { - dto.setPatientName(latestOrder.getPatientName()); - dto.setPatientId(String.valueOf(latestOrder.getPatientId())); - dto.setPhone(latestOrder.getPhone()); - } - // ----------------------- - } else if ("checked".equals(stopReason)) { - dto.setStatus("已取号"); - } else { - // 兜底逻辑:如果 is_stopped 为 true 但没有匹配到原因 - dto.setStatus("不可预约"); + // 拼接就诊时间 + 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())); + } catch (Exception e) { + dto.setAppointmentDate(new java.util.Date()); } - } else { - // is_stopped 为 false 或 null 时 - dto.setStatus("未预约"); } - // 费用处理 (挂号费 + 诊疗费) - int totalFee = schedule.getRegisterFee() + schedule.getDiagnosisFee(); - dto.setFee(String.valueOf(totalFee)); - - // 日期处理:LocalDateTime 转 Date - if (schedule.getCreateTime() != null) { - // 1. 先转成 Instant - Instant instant = schedule.getCreateTime().toInstant(); - // 2. 结合时区转成 ZonedDateTime - ZonedDateTime zdt = instant.atZone(ZoneId.systemDefault()); - // 3. 再转回 Date (如果 DTO 需要的是 Date) - dto.setAppointmentDate(Date.from(zdt.toInstant())); + // 精准状态翻译!把底层的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)) { + dto.setStatus("已停诊"); + } else { + dto.setStatus("未预约"); + } + } else { + dto.setStatus("未预约"); + } } tickets.add(dto); } } - // 3. 封装分页响应结构 - Map result = new HashMap<>(); + // 5. 按照前端组件需要的【真分页】格式进行包装,并返回 + java.util.Map result = new java.util.HashMap<>(); + result.put("list", tickets); + result.put("total", rawPage.getTotal()); // 这个 total 就是底层用 COUNT(*) 算出来的真实总条数! + result.put("page", query.getPage()); + result.put("limit", query.getLimit()); + + return R.ok(result); + } + + @Override + public R listDoctorAvailability(com.openhis.appointmentmanage.dto.TicketQueryDTO query) { + if (query == null) { + query = new com.openhis.appointmentmanage.dto.TicketQueryDTO(); + } + + java.util.List rawList = scheduleSlotMapper + .selectDoctorAvailabilitySummary(query); + java.util.List> doctors = new java.util.ArrayList<>(); + if (rawList != null) { + for (com.openhis.appointmentmanage.domain.DoctorAvailabilityDTO item : rawList) { + java.util.Map row = new java.util.HashMap<>(); + String doctorName = item.getDoctorName(); + Long doctorId = item.getDoctorId(); + row.put("id", doctorId != null ? String.valueOf(doctorId) : doctorName); + row.put("name", doctorName); + row.put("available", item.getAvailable() == null ? 0 : item.getAvailable()); + row.put("type", item.getTicketType() == null ? "general" : item.getTicketType()); + doctors.add(row); + } + } + return R.ok(doctors); + } + + @Override + public R listAllTickets() { + // 1. 调用最新的 Mapper,直接从数据库抽出我们半成品的 DTO(强类型!) + List rawDtos = scheduleSlotMapper.selectAllTicketSlots(); + + // 这是真正要发给前端展示的包裹外卖盒 + List tickets = new ArrayList<>(); + + if (rawDtos != null) { + for (com.openhis.appointmentmanage.domain.TicketSlotDTO raw : rawDtos) { + TicketDto dto = new TicketDto(); + + // --- 基础字段处理 --- + // 注意:这里已经变成了极其舒服的 .getSlotId() 方法调用,告别魔鬼字符串! + dto.setSlot_id(raw.getSlotId()); + dto.setBusNo(String.valueOf(raw.getSlotId())); // 暂时借用真实槽位ID做唯一流水号 + dto.setDoctor(raw.getDoctor()); + dto.setDepartment(raw.getDepartmentName()); + dto.setFee(raw.getFee()); + dto.setPatientName(raw.getPatientName()); + dto.setPatientId(raw.getPatientId() != null ? String.valueOf(raw.getPatientId()) : null); + dto.setPhone(raw.getPhone()); + + // --- 号源类型处理 (普通/专家) --- + // 改用底层 adm_doctor_schedule 传来的标准数字字典:0=普通,1=专家 + if (raw.getRegType() != null && raw.getRegType() == 1) { + dto.setTicketType("expert"); + } else { + dto.setTicketType("general"); + } + + // --- 就诊时间严谨拼接 --- + // 拼接出来给前端展示的,如 "2026-03-20 08:30" + 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())); + } catch (Exception e) { + dto.setAppointmentDate(new java.util.Date()); + } + } + + // --- 核心逻辑:精准状态分类 --- + // 第一关:底层硬性停诊拦截 + if (Boolean.TRUE.equals(raw.getIsStopped())) { + dto.setStatus("已停诊"); + } else { + // 第二关:看独立的细分槽位状态 (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("已停诊"); // 视业务可改回已取消 + } else { + dto.setStatus("未预约"); + } + } else { + dto.setStatus("未预约"); + } + } + + tickets.add(dto); + } + } + + // 3. 封装分页响应结构并吐给前端 + java.util.Map result = new java.util.HashMap<>(); result.put("list", tickets); result.put("total", tickets.size()); result.put("page", 1); result.put("limit", 20); + return R.ok(result); } @@ -268,7 +311,7 @@ public class TicketAppServiceImpl implements ITicketAppService { dto.setBusNo(ticket.getBusNo()); dto.setDepartment(ticket.getDepartment()); dto.setDoctor(ticket.getDoctor()); - + // 处理号源类型(转换为英文,前端期望的是general或expert) String ticketType = ticket.getTicketType(); if ("普通".equals(ticketType)) { @@ -278,10 +321,10 @@ public class TicketAppServiceImpl implements ITicketAppService { } else { dto.setTicketType(ticketType); } - + // 处理号源时间(dateTime) dto.setDateTime(ticket.getTime()); - + // 处理号源状态(转换为中文) String status = ticket.getStatus(); switch (status) { @@ -300,12 +343,12 @@ public class TicketAppServiceImpl implements ITicketAppService { default: dto.setStatus(status); } - + dto.setFee(ticket.getFee()); dto.setPatientName(ticket.getPatientName()); dto.setPatientId(ticket.getMedicalCard()); // 就诊卡号 dto.setPhone(ticket.getPhone()); - + // 获取患者性别 if (ticket.getPatientId() != null) { Patient patient = patientService.getById(ticket.getPatientId()); @@ -325,7 +368,7 @@ public class TicketAppServiceImpl implements ITicketAppService { } } } - + dto.setAppointmentDate(ticket.getAppointmentDate()); dto.setAppointmentTime(ticket.getAppointmentTime()); dto.setDepartmentId(ticket.getDepartmentId()); diff --git a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/controller/TicketController.java b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/controller/TicketController.java index ae5f5e6c..df64f985 100644 --- a/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/controller/TicketController.java +++ b/openhis-server-new/openhis-application/src/main/java/com/openhis/web/appointmentmanage/controller/TicketController.java @@ -3,8 +3,12 @@ package com.openhis.web.appointmentmanage.controller; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.core.common.annotation.Anonymous; import com.core.common.core.domain.R; +import com.openhis.appointmentmanage.domain.AppointmentBookDTO; +import com.openhis.appointmentmanage.dto.TicketQueryDTO; import com.openhis.web.appointmentmanage.appservice.ITicketAppService; import com.openhis.web.appointmentmanage.dto.TicketDto; + +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; @@ -19,11 +23,35 @@ import java.util.Map; @RequestMapping("/appointment/ticket") public class TicketController { + /** + * 分页查询门诊号源列表 (带多条件过滤) + * + * @param query 查询条件 + * @return 分页号源列表 + */ + @Anonymous + @PostMapping("/list") + public R listTicket(@RequestBody @Validated TicketQueryDTO query) { + return ticketAppService.listTicket(query); + } + + /** + * 查询医生余号汇总(基于号源池,不受分页影响) + * + * @param query 查询条件 + * @return 医生余号列表 + */ + @Anonymous + @PostMapping("/doctorSummary") + public R listDoctorAvailability(@RequestBody @Validated TicketQueryDTO query) { + return ticketAppService.listDoctorAvailability(query); + } + @Resource private ITicketAppService ticketAppService; - + /** - * 查询所有号源(用于测试) + * 查询所有号源 * * @return 所有号源列表 */ @@ -36,44 +64,44 @@ public class TicketController { /** * 预约号源 * - * @param params 预约参数 + * @param dto 预约参数 * @return 结果 */ @PostMapping("/book") - public R bookTicket(@RequestBody Map params) { - return ticketAppService.bookTicket(params); + public R bookTicket(@RequestBody @Validated AppointmentBookDTO dto) { + return ticketAppService.bookTicket(dto); } /** * 取消预约 * - * @param ticketId 号源ID + * @param slotId 槽位ID * @return 结果 */ @PostMapping("/cancel") - public R cancelTicket(@RequestParam Long ticketId) { - return ticketAppService.cancelTicket(ticketId); + public R cancelTicket(@RequestParam Long slotId) { + return ticketAppService.cancelTicket(slotId); } /** * 取号 * - * @param ticketId 号源ID + * @param slotId 槽位ID * @return 结果 */ @PostMapping("/checkin") - public R checkInTicket(@RequestParam Long ticketId) { - return ticketAppService.checkInTicket(ticketId); + public R checkInTicket(@RequestParam Long slotId) { + return ticketAppService.checkInTicket(slotId); } /** * 停诊 * - * @param ticketId 号源ID + * @param slotId 槽位ID * @return 结果 */ @PostMapping("/cancelConsultation") - public R cancelConsultation(@RequestParam Long ticketId) { - return ticketAppService.cancelConsultation(ticketId); + public R cancelConsultation(@RequestParam Long slotId) { + return ticketAppService.cancelConsultation(slotId); } } 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 f34335cc..6c3941a1 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 @@ -49,7 +49,7 @@ public class TicketDto { private String dateTime; /** - * 状态 (unbooked:未预约, booked:已预约, checked:已取号, cancelled:已取消, locked:已锁定) + * 状态 */ private String status; 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 36df4ff0..40313859 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 @@ -768,4 +768,28 @@ public class CommonConstants { Integer ACCOUNT_DEVICE_TYPE = 6; } + /** + * 号源槽位状态 (adm_schedule_slot.slot_status) + */ + public interface SlotStatus { + /** 可用 / 待预约 */ + Integer AVAILABLE = 0; + /** 已预约 */ + Integer BOOKED = 1; + /** 已停诊 / 已失效 */ + Integer STOPPED = 2; + } + + /** + * 预约订单状态 (order_main.status) + */ + public interface AppointmentOrderStatus { + /** 已预约 (待就诊) */ + Integer BOOKED = 1; + /** 已取号 (已就诊) */ + Integer CHECKED_IN = 2; + /** 已取消 */ + Integer CANCELLED = 3; + } + } diff --git a/openhis-server-new/openhis-domain/src/main/java/com/openhis/appointmentmanage/domain/AppointmentBookDTO.java b/openhis-server-new/openhis-domain/src/main/java/com/openhis/appointmentmanage/domain/AppointmentBookDTO.java new file mode 100644 index 00000000..a43c673a --- /dev/null +++ b/openhis-server-new/openhis-domain/src/main/java/com/openhis/appointmentmanage/domain/AppointmentBookDTO.java @@ -0,0 +1,40 @@ +package com.openhis.appointmentmanage.domain; + +import lombok.Data; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * 预约挂号提交表单 (防篡改设计) + */ +@Data +public class AppointmentBookDTO implements Serializable { + + @NotNull(message = "号源槽位ID不能为空") + private Long slotId; + + // 兼容前端发来的旧字段,即使发了我们底层也不用,防报错接收 + private Long ticketId; + + private Long patientId; + + @NotBlank(message = "患者姓名不能为空") + private String patientName; + + @NotBlank(message = "就诊卡号不能为空") + private String medicalCard; + + @NotBlank(message = "手机号不能为空") + private String phone; + + private Integer gender; + + // 前端传的 tenant_id,我们为了兼容它带下划线的写法 + private Integer tenant_id; + + // 前端还会强行发这俩危险字段,我们只管接收堵口子,到了后端全丢弃不用 + private BigDecimal fee; + private String regType; +} diff --git a/openhis-server-new/openhis-domain/src/main/java/com/openhis/appointmentmanage/domain/DoctorAvailabilityDTO.java b/openhis-server-new/openhis-domain/src/main/java/com/openhis/appointmentmanage/domain/DoctorAvailabilityDTO.java new file mode 100644 index 00000000..5c149d0e --- /dev/null +++ b/openhis-server-new/openhis-domain/src/main/java/com/openhis/appointmentmanage/domain/DoctorAvailabilityDTO.java @@ -0,0 +1,15 @@ +package com.openhis.appointmentmanage.domain; + +import lombok.Data; + +/** + * 医生余号汇总DTO(按号源池聚合) + */ +@Data +public class DoctorAvailabilityDTO { + private Long doctorId; + private String doctorName; + private Integer available; + private String ticketType; +} + 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 b6655748..de6af5a4 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 @@ -33,7 +33,7 @@ public class ScheduleSlot extends HisBaseEntity { private Integer status; /** 预约订单ID */ - private Integer orderId; + private Long orderId; /** 预计叫号时间 */ private LocalTime expectTime; 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 new file mode 100644 index 00000000..96377d1b --- /dev/null +++ b/openhis-server-new/openhis-domain/src/main/java/com/openhis/appointmentmanage/domain/TicketSlotDTO.java @@ -0,0 +1,42 @@ +package com.openhis.appointmentmanage.domain; + +import lombok.Data; +import java.time.LocalDate; +import java.time.LocalTime; + +/** + * 专门用于承接底层的号源池与具体槽位联查结果 (不对外暴露) + */ +@Data +public class TicketSlotDTO { + // 基础信息 + private Long slotId; + private Long scheduleId; + private String doctor; + private Long doctorId; + private Long departmentId; + private String departmentName; + private String fee; + private String patientName; + private String medicalCard; + private Long patientId; + private String phone; + private Integer orderStatus; + + // 底层逻辑判断专属字段 + private Integer slotStatus; + private LocalTime expectTime; + private LocalDate scheduleDate; + private Integer regType; + private Integer poolStatus; + private String stopReason; + private Boolean isStopped; + + public Boolean getIsStopped() { + return isStopped; + } + + public void setIsStopped(Boolean isStopped) { + this.isStopped = isStopped; + } +} diff --git a/openhis-server-new/openhis-domain/src/main/java/com/openhis/appointmentmanage/dto/TicketQueryDTO.java b/openhis-server-new/openhis-domain/src/main/java/com/openhis/appointmentmanage/dto/TicketQueryDTO.java new file mode 100644 index 00000000..429aa6df --- /dev/null +++ b/openhis-server-new/openhis-domain/src/main/java/com/openhis/appointmentmanage/dto/TicketQueryDTO.java @@ -0,0 +1,43 @@ +package com.openhis.appointmentmanage.dto; + +import lombok.Data; +import java.io.Serializable; + +/** + * 门诊预约挂号查询条件 DTO + */ +@Data +public class TicketQueryDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + // 看诊日期 (例如: 2026-03-25) + private String date; + + // 号源状态 (unbooked, booked, checked, cancelled, all) + private String status; + + // 号源类型 (general: 普通号, expert: 专家号) + private String type; + + // 科室名称 (例如: 内科) + private String department; + + // 医生ID + private Long doctorId; + + // 患者姓名 (模糊搜索) + private String name; + + // 就诊卡号 (模糊搜索) + private String card; + + // 手机号 (模糊搜索) + private String phone; + + // 当前页码 (默认第一页) + private Integer page = 1; + + // 每页显示条数 (默认查20条) + private Integer limit = 20; +} 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 3861f0df..77927d87 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 @@ -2,8 +2,39 @@ package com.openhis.appointmentmanage.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.openhis.appointmentmanage.domain.SchedulePool; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Update; import org.springframework.stereotype.Repository; @Repository public interface SchedulePoolMapper extends BaseMapper { + + /** + * 按号源池实时重算统计值,避免并发场景下计数漂移。 + * + * 说明:available_num 在当前项目中可能为数据库生成列,因此这里仅维护 + * booked_num / locked_num,剩余号由数据库或查询逻辑计算。 + */ + @Update(""" + UPDATE adm_schedule_pool p + SET + booked_num = COALESCE(( + SELECT COUNT(1) + FROM adm_schedule_slot s + WHERE s.pool_id = p.id + AND s.delete_flag = '0' + AND s.status = 1 + ), 0), + locked_num = COALESCE(( + SELECT COUNT(1) + FROM adm_schedule_slot s + WHERE s.pool_id = p.id + AND s.delete_flag = '0' + AND s.status = 3 + ), 0), + update_time = now() + WHERE p.id = #{poolId} + AND p.delete_flag = '0' + """) + int refreshPoolStats(@Param("poolId") Long 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 1e367f05..805e342f 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 @@ -1,10 +1,53 @@ package com.openhis.appointmentmanage.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.openhis.appointmentmanage.domain.DoctorAvailabilityDTO; 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.List; import org.springframework.stereotype.Repository; +import org.apache.ibatis.annotations.Param; @Repository public interface ScheduleSlotMapper extends BaseMapper { - // + // 多表查询排班信息展示来预约挂号 + List selectAllTicketSlots(); + + /** + * 根据槽位ID精确查出完整的聚合信息 + */ + TicketSlotDTO selectTicketSlotById(@Param("id") Long id); + + /** + * 原子抢占槽位:仅当当前状态=0(可用)时,更新为1(已预约)。 + */ + int lockSlotForBooking(@Param("slotId") Long slotId); + + /** + * 按主键更新槽位状态。 + */ + int updateSlotStatus(@Param("slotId") Long slotId, @Param("status") Integer status); + + /** + * 根据槽位ID查询所属号源池ID。 + */ + Long selectPoolIdBySlotId(@Param("slotId") Long slotId); + + /** + * 预约成功后,回填对应订单ID到号源槽位。 + */ + int bindOrderToSlot(@Param("slotId") Long slotId, @Param("orderId") Long orderId); + + /** + * 带分页和动态条件过滤的真实查询接口 + */ + Page selectTicketSlotsPage(Page page, @Param("query") TicketQueryDTO query); + + /** + * 按号源池聚合医生余号(不受分页影响)。 + */ + List selectDoctorAvailabilitySummary(@Param("query") TicketQueryDTO query); + } diff --git a/openhis-server-new/openhis-domain/src/main/java/com/openhis/clinical/service/ITicketService.java b/openhis-server-new/openhis-domain/src/main/java/com/openhis/clinical/service/ITicketService.java index 01810524..e91b4568 100644 --- a/openhis-server-new/openhis-domain/src/main/java/com/openhis/clinical/service/ITicketService.java +++ b/openhis-server-new/openhis-domain/src/main/java/com/openhis/clinical/service/ITicketService.java @@ -74,10 +74,10 @@ public interface ITicketService extends IService { /** * 预约号源 * - * @param params 预约参数 + * @param dto 预约参数 * @return 结果 */ - int bookTicket(Map params); + int bookTicket(com.openhis.appointmentmanage.domain.AppointmentBookDTO dto); /** * 取消预约 @@ -85,7 +85,7 @@ public interface ITicketService extends IService { * @param ticketId 号源ID * @return 结果 */ - int cancelTicket(Long ticketId); + int cancelTicket(Long slotId); /** * 取号 @@ -93,7 +93,7 @@ public interface ITicketService extends IService { * @param ticketId 号源ID * @return 结果 */ - int checkInTicket(Long ticketId); + int checkInTicket(Long slotId); /** * 停诊 @@ -101,5 +101,5 @@ public interface ITicketService extends IService { * @param ticketId 号源ID * @return 结果 */ - int cancelConsultation(Long ticketId); + int cancelConsultation(Long slotId); } 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 d6bc8e43..0543aff3 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 @@ -4,10 +4,9 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.core.common.utils.AssignSeqUtil; import com.openhis.clinical.domain.Order; -import com.openhis.clinical.domain.Ticket; import com.openhis.clinical.mapper.OrderMapper; -import com.openhis.clinical.mapper.TicketMapper; import com.openhis.clinical.service.IOrderService; +import com.openhis.common.constant.CommonConstants.AppointmentOrderStatus; import com.openhis.common.enums.AssignSeqEnum; import org.springframework.stereotype.Service; @@ -23,9 +22,6 @@ public class OrderServiceImpl extends ServiceImpl implements @Resource private OrderMapper orderMapper; - @Resource - private TicketMapper ticketMapper; - @Resource private AssignSeqUtil assignSeqUtil; @@ -86,25 +82,20 @@ public class OrderServiceImpl extends ServiceImpl implements @Override public Order createAppointmentOrder(Map params) { - Long slotId = params.get("slotId") != null ? Long.valueOf(params.get("slotId").toString()) : null; + Long slotId = (Long) params.get("slotId"); if (slotId == null) { throw new RuntimeException("号源ID不能为空"); } - Ticket ticket = ticketMapper.selectTicketById(slotId); - if (ticket == null) { - throw new RuntimeException("号源不存在"); - } - Order order = new Order(); String orderNo = assignSeqUtil.getSeq(AssignSeqEnum.ORDER_NUM.getPrefix(), 18); order.setOrderNo(orderNo); - Long patientId = params.get("patientId") != null ? Long.valueOf(params.get("patientId").toString()) : null; - String patientName = params.get("patientName") != null ? params.get("patientName").toString() : null; - String medicalCard = params.get("medicalCard") != null ? params.get("medicalCard").toString() : null; - String phone = params.get("phone") != null ? params.get("phone").toString() : null; - Integer gender = params.get("gender") != null ? Integer.valueOf(params.get("gender").toString()) : null; + Long patientId = (Long) params.get("patientId"); + String patientName = (String) params.get("patientName"); + String medicalCard = (String) params.get("medicalCard"); + String phone = (String) params.get("phone"); + Integer gender = (Integer) params.get("gender"); order.setPatientId(patientId); order.setPatientName(patientName); @@ -113,28 +104,31 @@ public class OrderServiceImpl extends ServiceImpl implements order.setGender(gender); order.setSlotId(slotId); - order.setDepartmentId(ticket.getDepartmentId()); - order.setDepartmentName(ticket.getDepartment()); - order.setDoctorId(ticket.getDoctorId()); - order.setDoctorName(ticket.getDoctor()); + order.setScheduleId((Long) params.get("scheduleId")); + order.setDepartmentId((Long) params.get("departmentId")); + order.setDepartmentName((String) params.get("departmentName")); + order.setDoctorId((Long) params.get("doctorId")); + order.setDoctorName((String) params.get("doctorName")); - String regType = params.get("regType") != null ? params.get("regType").toString() : "普通"; - order.setRegType(regType); + String regType = (String) params.get("regType"); + order.setRegType(regType != null ? regType : "普通"); - BigDecimal fee = params.get("fee") != null ? new BigDecimal(params.get("fee").toString()) : BigDecimal.ZERO; - order.setFee(fee); + BigDecimal fee = parseFee(params.get("fee")); + order.setFee(fee != null ? fee : BigDecimal.ZERO); - Date appointmentDate = new Date(); - order.setAppointmentDate(appointmentDate); - order.setAppointmentTime(new Date()); - - order.setStatus(1); + // appointmentDate / appointmentTime 由调用方(TicketServiceImpl)在强类型上下文中 + // 提前合并为 java.util.Date,此处直接使用,不再重复做 LocalDate + LocalTime 类型转换。 + Date appointmentDateTime = params.get("appointmentDate") instanceof Date + ? (Date) params.get("appointmentDate") + : new Date(); // 兜底:正常业务不应走到这里 + order.setAppointmentDate(appointmentDateTime); + order.setAppointmentTime(appointmentDateTime); + order.setStatus(AppointmentOrderStatus.BOOKED); order.setPayStatus(0); order.setVersion(0); // 设置租户ID - Integer tenantId = params.get("tenant_id") != null ? Integer.valueOf(params.get("tenant_id").toString()) : null; - order.setTenantId(tenantId); + order.setTenantId((Integer) params.get("tenant_id")); order.setCreateTime(new Date()); order.setUpdateTime(new Date()); @@ -144,16 +138,40 @@ public class OrderServiceImpl extends ServiceImpl implements return order; } + private BigDecimal parseFee(Object feeObj) { + if (feeObj == null) { + return BigDecimal.ZERO; + } + if (feeObj instanceof BigDecimal) { + return (BigDecimal) feeObj; + } + if (feeObj instanceof Number) { + return BigDecimal.valueOf(((Number) feeObj).doubleValue()); + } + if (feeObj instanceof String) { + String feeStr = ((String) feeObj).trim(); + if (feeStr.isEmpty()) { + return BigDecimal.ZERO; + } + try { + return new BigDecimal(feeStr); + } catch (NumberFormatException e) { + throw new RuntimeException("挂号费格式错误: " + feeStr); + } + } + throw new RuntimeException("挂号费类型错误: " + feeObj.getClass().getName()); + } + @Override public int cancelAppointmentOrder(Long orderId, String cancelReason) { Order order = orderMapper.selectOrderById(orderId); if (order == null) { throw new RuntimeException("订单不存在"); } - if (order.getStatus() == 3) { + if (AppointmentOrderStatus.CANCELLED.equals(order.getStatus())) { throw new RuntimeException("订单已取消"); } - if (order.getStatus() == 2) { + if (AppointmentOrderStatus.CHECKED_IN.equals(order.getStatus())) { throw new RuntimeException("订单已完成,无法取消"); } 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 08e456c0..e9bbb715 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 @@ -2,17 +2,27 @@ package com.openhis.clinical.service.impl; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.openhis.appointmentmanage.domain.TicketSlotDTO; +import com.openhis.appointmentmanage.mapper.SchedulePoolMapper; +import com.openhis.appointmentmanage.mapper.ScheduleSlotMapper; import com.openhis.clinical.domain.Order; import com.openhis.clinical.domain.Ticket; import com.openhis.clinical.mapper.TicketMapper; import com.openhis.clinical.service.IOrderService; import com.openhis.clinical.service.ITicketService; +import com.openhis.common.constant.CommonConstants.AppointmentOrderStatus; +import com.openhis.common.constant.CommonConstants.SlotStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; import java.util.Date; import java.util.List; import java.util.Map; @@ -33,6 +43,12 @@ public class TicketServiceImpl extends ServiceImpl impleme @Resource private IOrderService orderService; + @Resource + private ScheduleSlotMapper scheduleSlotMapper; + + @Resource + private SchedulePoolMapper schedulePoolMapper; + /** * 查询号源列表 * @@ -47,7 +63,7 @@ public class TicketServiceImpl extends ServiceImpl impleme /** * 分页查询号源列表 * - * @param page 分页参数 + * @param page 分页参数 * @param ticket 号源信息 * @return 号源集合 */ @@ -114,154 +130,183 @@ public class TicketServiceImpl extends ServiceImpl impleme /** * 预约号源 * - * @param params 预约参数 + * @param dto 预约参数 * @return 结果 */ @Override @Transactional(rollbackFor = Exception.class) - public int bookTicket(Map params) { - Long ticketId = Long.valueOf(params.get("ticketId").toString()); - Long patientId = params.get("patientId") != null ? Long.valueOf(params.get("patientId").toString()) : null; - String patientName = params.get("patientName") != null ? params.get("patientName").toString() : null; - String medicalCard = params.get("medicalCard") != null ? params.get("medicalCard").toString() : null; - String phone = params.get("phone") != null ? params.get("phone").toString() : null; - - logger.debug("开始预约号源,ticketId: {}, patientId: {}, patientName: {}", ticketId, patientId, patientName); - - Ticket ticket = ticketMapper.selectTicketById(ticketId); - if (ticket == null) { - logger.error("号源不存在,ticketId: {}", ticketId); - throw new RuntimeException("号源不存在"); + public int bookTicket(com.openhis.appointmentmanage.domain.AppointmentBookDTO dto) { + Long slotId = dto.getSlotId(); + + logger.debug("开始执行纯净打单路线,slotId: {}, patientName: {}", slotId, dto.getPatientName()); + + // 1. 直查物理大底座! + TicketSlotDTO slot = scheduleSlotMapper.selectTicketSlotById(slotId); + + if (slot == null) { + logger.error("安全拦截:号源底库核对失败,slotId: {}", slotId); + throw new RuntimeException("号源数据不存在"); } - - logger.debug("查询到号源信息,id: {}, status: {}, deleteFlag: {}", ticket.getId(), ticket.getStatus(), ticket.getDeleteFlag()); - - // 详细调试:检查状态字符串的详细信息 - String status = ticket.getStatus(); - logger.debug("状态字符串详细信息: value='{}', length={}, isNull={}", status, status != null ? status.length() : "null", status == null); - if (status != null) { - StringBuilder charInfo = new StringBuilder(); - for (int i = 0; i < status.length(); i++) { - charInfo.append(status.charAt(i)).append("(").append((int) status.charAt(i)).append(") "); - } - logger.debug("状态字符串字符信息: {}", charInfo.toString()); + if (slot.getSlotStatus() != null && !SlotStatus.AVAILABLE.equals(slot.getSlotStatus())) { + throw new RuntimeException("手慢了!该号源已刚刚被他人抢占"); } - - // 详细调试:检查每个状态比较的结果 - boolean isUnbooked = "unbooked".equals(status); - boolean isLocked = "locked".equals(status); - boolean isCancelled = "cancelled".equals(status); - boolean isChecked = "checked".equals(status); - boolean isBooked = "booked".equals(status); - logger.debug("状态比较结果: unbooked={}, locked={}, cancelled={}, checked={}, booked={}", - isUnbooked, isLocked, isCancelled, isChecked, isBooked); - - if (!isUnbooked && !isLocked && !isCancelled && !isChecked && !isBooked) { - logger.error("号源不可预约,id: {}, status: {}", ticket.getId(), ticket.getStatus()); - throw new RuntimeException("号源不可预约"); + if (Boolean.TRUE.equals(slot.getIsStopped())) { + throw new RuntimeException("该排班医生已停诊"); } - params.put("slotId", ticketId); - Order order = orderService.createAppointmentOrder(params); + // 原子抢占:避免并发下同一槽位被重复预约 + int lockRows = scheduleSlotMapper.lockSlotForBooking(slotId); + if (lockRows <= 0) { + throw new RuntimeException("手慢了!该号源已刚刚被他人抢占"); + } - Ticket updateTicket = new Ticket(); - updateTicket.setId(ticketId); - updateTicket.setStatus("booked"); - updateTicket.setPatientId(patientId); - updateTicket.setPatientName(patientName); - updateTicket.setMedicalCard(medicalCard); - updateTicket.setPhone(phone); - updateTicket.setAppointmentDate(new Date()); - updateTicket.setAppointmentTime(new Date()); - - int result = ticketMapper.updateById(updateTicket); - logger.debug("预约成功,更新号源状态为booked,result: {}", result); - - return result; + // 2. 将 DTO 安全降级转换为 Map 给最底层的建单工厂 + // 因为建单方法非常底层,通用性强,为了不影响它,我们在这里安全组装 Map + + // 在持有强类型 DTO 的地方提前合并日期+时间,转为 java.util.Date 后再传入 Map, + // 避免将 LocalDate/LocalTime 装箱为 Object 跨层传递时可能出现的类型映射失败(如 MyBatis 将 + // PostgreSQL date 类型映射为 java.sql.Date 导致 slot.getScheduleDate() 返回 null)。 + LocalDate scheduleDate = slot.getScheduleDate(); + LocalTime expectTime = slot.getExpectTime(); + Date appointmentDateTime; + if (scheduleDate != null && expectTime != null) { + LocalDateTime ldt = LocalDateTime.of(scheduleDate, expectTime); + appointmentDateTime = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant()); + } else { + // 理论上不应走到这里,若走到说明号源池数据异常 + appointmentDateTime = new Date(); + } + + Map safeParams = new java.util.HashMap<>(); + safeParams.put("slotId", slotId); + safeParams.put("scheduleId", slot.getScheduleId()); + // 直接传入已合并的 Date 对象,OrderServiceImpl 无需再做类型转换 + safeParams.put("appointmentDate", appointmentDateTime); + safeParams.put("appointmentTime", appointmentDateTime); + safeParams.put("patientId", dto.getPatientId()); + safeParams.put("patientName", dto.getPatientName()); + safeParams.put("medicalCard", dto.getMedicalCard()); + safeParams.put("phone", dto.getPhone()); + safeParams.put("gender", dto.getGender()); + safeParams.put("tenant_id", dto.getTenant_id()); + + // 3. 【绝对防御】:强制覆盖!不管前端 DTO 传了什么鬼,全以底层数据库物理表为准! + safeParams.put("departmentId", slot.getDepartmentId()); + safeParams.put("departmentName", slot.getDepartmentName()); + safeParams.put("doctorId", slot.getDoctorId()); + safeParams.put("doctorName", slot.getDoctor()); + safeParams.put("fee", toBigDecimal(slot.getFee())); + safeParams.put("regType", slot.getRegType() != null && slot.getRegType() == 1 ? "专家" : "普通"); + + // 4. 收银台建单! + Order order = orderService.createAppointmentOrder(safeParams); + if (order == null || order.getId() == null) { + throw new RuntimeException("预约订单创建失败"); + } + + // 5. 回填订单ID到号源槽位,保证号源与订单一一关联 + int bindRows = scheduleSlotMapper.bindOrderToSlot(slotId, order.getId()); + if (bindRows <= 0) { + throw new RuntimeException("预约成功但号源回填订单失败,请重试"); + } + + refreshPoolStatsBySlotId(slotId); + return 1; } /** * 取消预约 * - * @param ticketId 号源ID + * @param slotId 槽位ID * @return 结果 */ @Override @Transactional(rollbackFor = Exception.class) - public int cancelTicket(Long ticketId) { - Ticket ticket = ticketMapper.selectTicketById(ticketId); - if (ticket == null) { - throw new RuntimeException("号源不存在"); + public int cancelTicket(Long slotId) { + TicketSlotDTO slot = scheduleSlotMapper.selectTicketSlotById(slotId); + if (slot == null) { + throw new RuntimeException("号源槽位不存在"); } - if (!"booked".equals(ticket.getStatus()) && !"locked".equals(ticket.getStatus())) { + if (slot.getSlotStatus() == null || !SlotStatus.BOOKED.equals(slot.getSlotStatus())) { throw new RuntimeException("号源不可取消预约"); } - List orders = orderService.selectOrderBySlotId(ticketId); - for(Order order:orders){ + List orders = orderService.selectOrderBySlotId(slotId); + if (orders == null || orders.isEmpty()) { + throw new RuntimeException("当前号源没有可取消的预约订单"); + } + for (Order order : orders) { orderService.cancelAppointmentOrder(order.getId(), "患者取消预约"); } - ticket.setStatus("unbooked"); - ticket.setPatientId(null); - ticket.setPatientName(null); - ticket.setMedicalCard(null); - ticket.setPhone(null); - ticket.setAppointmentDate(null); - ticket.setAppointmentTime(null); - return ticketMapper.updateTicket(ticket); + int updated = scheduleSlotMapper.updateSlotStatus(slotId, SlotStatus.AVAILABLE); + if (updated > 0) { + refreshPoolStatsBySlotId(slotId); + } + return updated; } /** * 取号 * - * @param ticketId 号源ID + * @param slotId 槽位ID * @return 结果 */ @Override - public int checkInTicket(Long ticketId) { - // 获取号源信息 - Ticket ticket = ticketMapper.selectTicketById(ticketId); - if (ticket == null) { - throw new RuntimeException("号源不存在"); + @Transactional(rollbackFor = Exception.class) + public int checkInTicket(Long slotId) { + List orders = orderService.selectOrderBySlotId(slotId); + if (orders == null || orders.isEmpty()) { + throw new RuntimeException("当前号源没有可取号的预约订单"); } - if (!"booked".equals(ticket.getStatus()) && !"locked".equals(ticket.getStatus())) { - throw new RuntimeException("号源不可取号"); - } - // 更新号源状态为已取号 - ticket.setStatus("checked"); - return ticketMapper.updateTicket(ticket); + Order latestOrder = orders.get(0); + return orderService.updateOrderStatusById(latestOrder.getId(), AppointmentOrderStatus.CHECKED_IN); } /** * 停诊 * - * @param ticketId 号源ID + * @param slotId 槽位ID * @return 结果 */ @Override @Transactional(rollbackFor = Exception.class) - public int cancelConsultation(Long ticketId) { - // 获取号源信息 - Ticket ticket = ticketMapper.selectTicketById(ticketId); - if (ticket == null) { - throw new RuntimeException("号源不存在"); + public int cancelConsultation(Long slotId) { + TicketSlotDTO slot = scheduleSlotMapper.selectTicketSlotById(slotId); + if (slot == null) { + throw new RuntimeException("号源槽位不存在"); } - - // 检查是否存在相关订单,如果存在则取消 - List orders = orderService.selectOrderBySlotId(ticketId); - for(Order order:orders){ + + List orders = orderService.selectOrderBySlotId(slotId); + for (Order order : orders) { orderService.cancelAppointmentOrder(order.getId(), "医生停诊"); } - - // 更新号源状态为已取消 - ticket.setStatus("cancelled"); - ticket.setPatientId(null); - ticket.setPatientName(null); - ticket.setMedicalCard(null); - ticket.setPhone(null); - ticket.setAppointmentDate(null); - ticket.setAppointmentTime(null); - return ticketMapper.updateTicket(ticket); + + int updated = scheduleSlotMapper.updateSlotStatus(slotId, SlotStatus.STOPPED); + if (updated > 0) { + refreshPoolStatsBySlotId(slotId); + } + return updated; + } + + /** + * 根据槽位ID找到号源池并刷新 booked_num / locked_num 统计。 + */ + private void refreshPoolStatsBySlotId(Long slotId) { + Long poolId = scheduleSlotMapper.selectPoolIdBySlotId(slotId); + if (poolId != null) { + schedulePoolMapper.refreshPoolStats(poolId); + } + } + + private BigDecimal toBigDecimal(String fee) { + if (fee == null || fee.trim().isEmpty()) { + return BigDecimal.ZERO; + } + try { + return new BigDecimal(fee.trim()); + } catch (NumberFormatException e) { + throw new RuntimeException("挂号费格式错误: " + fee); + } } } 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 new file mode 100644 index 00000000..2df3176c --- /dev/null +++ b/openhis-server-new/openhis-domain/src/main/resources/mapper/administration/ScheduleSlotMapper.xml @@ -0,0 +1,309 @@ + + + + + + + + + + + UPDATE adm_schedule_slot + SET + status = 1, + update_time = now() + WHERE + id = #{slotId} + AND status = 0 + AND delete_flag = '0' + + + + UPDATE adm_schedule_slot + SET + status = #{status}, + + order_id = NULL, + + update_time = now() + WHERE + id = #{slotId} + AND delete_flag = '0' + + + + + + UPDATE adm_schedule_slot + SET + order_id = #{orderId}, + update_time = now() + WHERE + id = #{slotId} + AND status = 1 + AND delete_flag = '0' + + + + + + + + diff --git a/openhis-ui-vue3/src/api/appoinmentmanage/dept.js b/openhis-ui-vue3/src/api/appoinmentmanage/dept.js index 2fc59422..e5adaac4 100644 --- a/openhis-ui-vue3/src/api/appoinmentmanage/dept.js +++ b/openhis-ui-vue3/src/api/appoinmentmanage/dept.js @@ -9,7 +9,16 @@ export function listDept(query) { }) } -// 查询科室详细 +// 获取挂号科室列表(与排班管理一致的数据源) +export function listRegisterOrganizations(query) { + return request({ + url: '/base-data-manage/organization/register-organizations', + method: 'get', + params: query + }) +} + +// 查询科室详情 export function getDept(deptId) { return request({ url: '/dept/' + deptId, diff --git a/openhis-ui-vue3/src/api/appoinmentmanage/ticket.js b/openhis-ui-vue3/src/api/appoinmentmanage/ticket.js index dd3834d3..7e3bbf06 100644 --- a/openhis-ui-vue3/src/api/appoinmentmanage/ticket.js +++ b/openhis-ui-vue3/src/api/appoinmentmanage/ticket.js @@ -9,6 +9,15 @@ export function listTicket(query) { }) } +// 查询医生余号汇总(基于号源池,不受分页影响) +export function listDoctorSummary(query) { + return request({ + url: '/appointment/ticket/doctorSummary', + method: 'post', + data: query + }) +} + // 预约号源 export function bookTicket(data) { return request({ @@ -19,29 +28,29 @@ export function bookTicket(data) { } // 取消预约 -export function cancelTicket(ticketId) { +export function cancelTicket(slotId) { return request({ url: '/appointment/ticket/cancel', method: 'post', - params: { ticketId } + params: { slotId } }) } // 取号 -export function checkInTicket(ticketId) { +export function checkInTicket(slotId) { return request({ url: '/appointment/ticket/checkin', method: 'post', - params: { ticketId } + params: { slotId } }) } // 停诊 -export function cancelConsultation(ticketId) { +export function cancelConsultation(slotId) { return request({ url: '/appointment/ticket/cancelConsultation', method: 'post', - params: { ticketId } + params: { slotId } }) } diff --git a/openhis-ui-vue3/src/views/appoinmentmanage/outpatientAppointment/index.vue b/openhis-ui-vue3/src/views/appoinmentmanage/outpatientAppointment/index.vue index 81b85466..94733ac0 100644 --- a/openhis-ui-vue3/src/views/appoinmentmanage/outpatientAppointment/index.vue +++ b/openhis-ui-vue3/src/views/appoinmentmanage/outpatientAppointment/index.vue @@ -31,7 +31,7 @@ />
- @@ -109,24 +109,24 @@
{{ item.dateTime }}
-
+
{{ item.status }}
{{ item.doctor }}
- + +
+ 科室:{{ item.department || '未知科室' }} +
+
挂号费:{{ item.fee }}元
- +
{{ item.ticketType === 'general' ? '普通' : '专家' }}
- +
{{ item.patientName }}({{ item.patientId }})
- -
- 电话号码: {{ item.phone }} -
-
- 加载中... -
-
- 没有更多数据了 -
-
+
+ +
+
暂无号源数据
@@ -155,11 +161,17 @@