232 预约管理-》门诊预约挂号:打开界面报错且无医生排班预约号源数据

This commit is contained in:
HuangXinQuan
2026-03-26 17:09:08 +08:00
parent 3f0fa3bbb3
commit 11cf88fd49
22 changed files with 1411 additions and 764 deletions

View File

@@ -74,10 +74,10 @@ public interface ITicketService extends IService<Ticket> {
/**
* 预约号源
*
* @param params 预约参数
* @param dto 预约参数
* @return 结果
*/
int bookTicket(Map<String, Object> params);
int bookTicket(com.openhis.appointmentmanage.domain.AppointmentBookDTO dto);
/**
* 取消预约
@@ -85,7 +85,7 @@ public interface ITicketService extends IService<Ticket> {
* @param ticketId 号源ID
* @return 结果
*/
int cancelTicket(Long ticketId);
int cancelTicket(Long slotId);
/**
* 取号
@@ -93,7 +93,7 @@ public interface ITicketService extends IService<Ticket> {
* @param ticketId 号源ID
* @return 结果
*/
int checkInTicket(Long ticketId);
int checkInTicket(Long slotId);
/**
* 停诊
@@ -101,5 +101,5 @@ public interface ITicketService extends IService<Ticket> {
* @param ticketId 号源ID
* @return 结果
*/
int cancelConsultation(Long ticketId);
int cancelConsultation(Long slotId);
}

View File

@@ -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<OrderMapper, Order> implements
@Resource
private OrderMapper orderMapper;
@Resource
private TicketMapper ticketMapper;
@Resource
private AssignSeqUtil assignSeqUtil;
@@ -86,25 +82,20 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
@Override
public Order createAppointmentOrder(Map<String, Object> 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<OrderMapper, Order> 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<OrderMapper, Order> 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("订单已完成,无法取消");
}

View File

@@ -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<TicketMapper, Ticket> impleme
@Resource
private IOrderService orderService;
@Resource
private ScheduleSlotMapper scheduleSlotMapper;
@Resource
private SchedulePoolMapper schedulePoolMapper;
/**
* 查询号源列表
*
@@ -47,7 +63,7 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
/**
* 分页查询号源列表
*
* @param page 分页参数
* @param page 分页参数
* @param ticket 号源信息
* @return 号源集合
*/
@@ -114,154 +130,183 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
/**
* 预约号源
*
* @param params 预约参数
* @param dto 预约参数
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int bookTicket(Map<String, Object> 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("预约成功更新号源状态为bookedresult: {}", 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<String, Object> 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<Order> orders = orderService.selectOrderBySlotId(ticketId);
for(Order order:orders){
List<Order> 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<Order> 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<Order> orders = orderService.selectOrderBySlotId(ticketId);
for(Order order:orders){
List<Order> 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);
}
}
}