|
@@ -0,0 +1,290 @@
|
|
|
|
|
+package com.ylx.massage.service.impl;
|
|
|
|
|
+
|
|
|
|
|
+import cn.hutool.core.util.StrUtil;
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
|
|
+import com.ylx.common.exception.ServiceException;
|
|
|
|
|
+import com.ylx.massage.domain.TOrder;
|
|
|
|
|
+import com.ylx.massage.enums.OrderStatusEnum;
|
|
|
|
|
+import com.ylx.massage.service.OrderValidationService;
|
|
|
|
|
+import com.ylx.massage.service.TOrderService;
|
|
|
|
|
+import com.ylx.massage.utils.OrderTimeRangeUtils;
|
|
|
|
|
+import org.slf4j.Logger;
|
|
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+
|
|
|
|
|
+import java.time.LocalDateTime;
|
|
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
|
|
+import java.util.Arrays;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 订单校验服务实现类
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 实现订单创建和接单时的校验功能,基于订单状态锁机制。
|
|
|
|
|
+ * 确保技师服务时间的互斥性,防止冲突订单的产生。
|
|
|
|
|
+ * </p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * @author ylx
|
|
|
|
|
+ * @version 1.0
|
|
|
|
|
+ * @since 2024
|
|
|
|
|
+ */
|
|
|
|
|
+@Service
|
|
|
|
|
+public class OrderValidationServiceImpl implements OrderValidationService {
|
|
|
|
|
+
|
|
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(OrderValidationServiceImpl.class);
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 进行中的订单状态列表
|
|
|
|
|
+ * 包括:已接单(1)、已到达(2)、服务中(3)
|
|
|
|
|
+ */
|
|
|
|
|
+ private static final List<Integer> IN_PROGRESS_STATUSES = Arrays.asList(
|
|
|
|
|
+ OrderStatusEnum.RECEIVED_ORDER.getCode(), // 1 - 已接单
|
|
|
|
|
+ OrderStatusEnum.ARRIVED.getCode(), // 2 - 已到达
|
|
|
|
|
+ OrderStatusEnum.SERVICE.getCode() // 3 - 服务中
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ private final TOrderService orderService;
|
|
|
|
|
+
|
|
|
|
|
+ public OrderValidationServiceImpl(TOrderService orderService) {
|
|
|
|
|
+ this.orderService = orderService;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 检查技师是否可以接单
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 基于订单状态锁机制和时间范围校验,检查技师是否有进行中的订单。
|
|
|
|
|
+ * 如果技师有进行中的订单,进一步检查时间范围是否重叠。
|
|
|
|
|
+ * </p>
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 时间范围校验逻辑:
|
|
|
|
|
+ * 1. 如果现有订单有明确的时间范围(开始时间、结束时间),检查是否与新订单重叠
|
|
|
|
|
+ * 2. 如果现有订单时间信息不完整,采用保守策略,拒绝接单
|
|
|
|
|
+ * 3. 考虑30分钟缓冲时间,在两个订单之间留出缓冲期
|
|
|
|
|
+ * </p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param technicianId 技师ID
|
|
|
|
|
+ * @param newOrder 新订单信息
|
|
|
|
|
+ * @return boolean true-可以接单
|
|
|
|
|
+ * @throws ServiceException 当技师不能接单时抛出异常
|
|
|
|
|
+ */
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public boolean canAcceptOrder(String technicianId, TOrder newOrder) {
|
|
|
|
|
+ // 1. 参数校验
|
|
|
|
|
+ if (StrUtil.isBlank(technicianId)) {
|
|
|
|
|
+ throw new ServiceException("技师ID不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 升级订单特殊处理(升级订单可以创建)
|
|
|
|
|
+ if (OrderTimeRangeUtils.isUpgradeOrder(newOrder)) {
|
|
|
|
|
+ log.info("升级订单,跳过互斥校验,订单号:{}", newOrder.getOrderNo());
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 查询技师的进行中订单
|
|
|
|
|
+ LambdaQueryWrapper<TOrder> queryWrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
+ queryWrapper.eq(TOrder::getcJsId, technicianId)
|
|
|
|
|
+ .in(TOrder::getnStatus, IN_PROGRESS_STATUSES)
|
|
|
|
|
+ .eq(TOrder::getIsDelete, 0);
|
|
|
|
|
+
|
|
|
|
|
+ List<TOrder> inProgressOrders = orderService.list(queryWrapper);
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 如果没有进行中的订单,可以接单
|
|
|
|
|
+ if (inProgressOrders == null || inProgressOrders.isEmpty()) {
|
|
|
|
|
+ log.info("技师 {} 没有进行中的订单,可以接单", technicianId);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 有进行中的订单,进一步检查时间范围
|
|
|
|
|
+ log.info("技师 {} 有 {} 个进行中的订单,开始时间范围校验", technicianId, inProgressOrders.size());
|
|
|
|
|
+
|
|
|
|
|
+ // 估算新订单的开始和结束时间
|
|
|
|
|
+ LocalDateTime newOrderStart = OrderTimeRangeUtils.estimateStartTime(newOrder);
|
|
|
|
|
+ LocalDateTime newOrderEnd = OrderTimeRangeUtils.estimateEndTime(newOrder);
|
|
|
|
|
+
|
|
|
|
|
+ // 创建临时订单对象用于时间范围校验
|
|
|
|
|
+ TOrder newOrderWithTime = new TOrder();
|
|
|
|
|
+ newOrderWithTime.setOrderNo(newOrder.getOrderNo());
|
|
|
|
|
+ newOrderWithTime.setcGoods(newOrder.getcGoods());
|
|
|
|
|
+ newOrderWithTime.setStartTime(newOrderStart);
|
|
|
|
|
+ newOrderWithTime.setEndTime(newOrderEnd);
|
|
|
|
|
+
|
|
|
|
|
+ // 遍历所有进行中的订单,检查时间范围是否重叠
|
|
|
|
|
+ for (TOrder existingOrder : inProgressOrders) {
|
|
|
|
|
+ if (OrderTimeRangeUtils.isTimeRangeOverlapping(newOrderWithTime, existingOrder)) {
|
|
|
|
|
+ // 时间范围重叠,构建详细错误信息
|
|
|
|
|
+ String message = buildTimeConflictMessage(existingOrder, newOrderWithTime);
|
|
|
|
|
+ log.warn("技师 {} 无法接单,原因:{}", technicianId, message);
|
|
|
|
|
+ throw new ServiceException(message);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 6. 所有进行中的订单都与新订单时间不冲突,可以接单
|
|
|
|
|
+ log.info("技师 {} 所有进行中订单的时间范围与新订单不冲突,可以接单", technicianId);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 检查技师是否可以被手动分配订单
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 用于后台管理员手动分配订单的场景。
|
|
|
|
|
+ * 与接单校验逻辑相同,技师不能有进行中的订单,且时间范围不能重叠。
|
|
|
|
|
+ * </p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param technicianId 技师ID
|
|
|
|
|
+ * @return true-可以分配
|
|
|
|
|
+ * @throws ServiceException 当技师不能被分配订单时抛出异常
|
|
|
|
|
+ */
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public boolean canManualAssign(String technicianId) {
|
|
|
|
|
+ // 1. 参数校验
|
|
|
|
|
+ if (StrUtil.isBlank(technicianId)) {
|
|
|
|
|
+ throw new ServiceException("技师ID不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 查询技师的进行中订单
|
|
|
|
|
+ LambdaQueryWrapper<TOrder> queryWrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
+ queryWrapper.eq(TOrder::getcJsId, technicianId)
|
|
|
|
|
+ .in(TOrder::getnStatus, IN_PROGRESS_STATUSES)
|
|
|
|
|
+ .eq(TOrder::getIsDelete, 0);
|
|
|
|
|
+
|
|
|
|
|
+ List<TOrder> inProgressOrders = orderService.list(queryWrapper);
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 如果没有进行中的订单,可以分配
|
|
|
|
|
+ if (inProgressOrders == null || inProgressOrders.isEmpty()) {
|
|
|
|
|
+ log.info("技师 {} 没有进行中的订单,可以手动分配订单", technicianId);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 有进行中的订单,检查时间范围
|
|
|
|
|
+ log.info("技师 {} 有 {} 个进行中的订单,无法手动分配", technicianId, inProgressOrders.size());
|
|
|
|
|
+
|
|
|
|
|
+ TOrder currentOrder = inProgressOrders.get(0);
|
|
|
|
|
+ String statusDesc = OrderStatusEnum.getDescByCode(currentOrder.getnStatus());
|
|
|
|
|
+
|
|
|
|
|
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
|
|
|
|
+ StringBuilder message = new StringBuilder();
|
|
|
|
|
+ message.append(String.format("该技师当前有%s的订单(订单号:%s),无法手动分配。\n",
|
|
|
|
|
+ statusDesc, currentOrder.getOrderNo()));
|
|
|
|
|
+
|
|
|
|
|
+ // 添加时间信息
|
|
|
|
|
+ if (currentOrder.getStartTime() != null) {
|
|
|
|
|
+ message.append("开始时间:").append(currentOrder.getStartTime().format(formatter)).append("\n");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (currentOrder.getEndTime() != null) {
|
|
|
|
|
+ message.append("结束时间:").append(currentOrder.getEndTime().format(formatter)).append("\n");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ message.append("请等待当前订单完成后再试。");
|
|
|
|
|
+
|
|
|
|
|
+ log.warn("技师 {} 无法手动分配订单,原因:{}", technicianId, message);
|
|
|
|
|
+ throw new ServiceException(message.toString());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取技师的进行中订单数量
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 统计技师当前进行中的订单数量。
|
|
|
|
|
+ * 用于前端展示技师状态和负载情况。
|
|
|
|
|
+ * </p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param technicianId 技师ID
|
|
|
|
|
+ * @return 进行中的订单数量
|
|
|
|
|
+ */
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public long getInProgressOrderCount(String technicianId) {
|
|
|
|
|
+ if (StrUtil.isBlank(technicianId)) {
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ LambdaQueryWrapper<TOrder> queryWrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
+ queryWrapper.eq(TOrder::getcJsId, technicianId)
|
|
|
|
|
+ .in(TOrder::getnStatus, IN_PROGRESS_STATUSES)
|
|
|
|
|
+ .eq(TOrder::getIsDelete, 0);
|
|
|
|
|
+
|
|
|
|
|
+ return orderService.count(queryWrapper);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 检查订单是否可以支付
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 防止在订单状态发生变化后仍然可以支付。
|
|
|
|
|
+ * 只有待付款状态的订单才可以支付。
|
|
|
|
|
+ * </p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param orderNo 订单号
|
|
|
|
|
+ * @return true-可以支付
|
|
|
|
|
+ * @throws ServiceException 当订单不可支付时抛出异常
|
|
|
|
|
+ */
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public boolean canPayOrder(String orderNo) {
|
|
|
|
|
+ if (StrUtil.isBlank(orderNo)) {
|
|
|
|
|
+ throw new ServiceException("订单号不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ LambdaQueryWrapper<TOrder> queryWrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
+ queryWrapper.eq(TOrder::getOrderNo, orderNo)
|
|
|
|
|
+ .eq(TOrder::getIsDelete, 0);
|
|
|
|
|
+
|
|
|
|
|
+ TOrder order = orderService.getOne(queryWrapper);
|
|
|
|
|
+
|
|
|
|
|
+ if (order == null) {
|
|
|
|
|
+ throw new ServiceException("订单不存在");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 只有待付款状态可以支付
|
|
|
|
|
+ if (!OrderStatusEnum.WAIT_PAY.getCode().equals(order.getnStatus())) {
|
|
|
|
|
+ String currentStatus = OrderStatusEnum.getDescByCode(order.getnStatus());
|
|
|
|
|
+ throw new ServiceException(
|
|
|
|
|
+ String.format("订单当前状态为【%s】,无法支付。只有待付款状态的订单才可以支付。", currentStatus)
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 构建时间冲突的详细错误信息
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 当检测到时间范围重叠时,生成包含详细时间信息的错误消息。
|
|
|
|
|
+ * </p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param existingOrder 现有订单
|
|
|
|
|
+ * @param newOrder 新订单
|
|
|
|
|
+ * @return String 错误信息
|
|
|
|
|
+ */
|
|
|
|
|
+ private String buildTimeConflictMessage(TOrder existingOrder, TOrder newOrder) {
|
|
|
|
|
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
|
|
|
|
+
|
|
|
|
|
+ StringBuilder message = new StringBuilder();
|
|
|
|
|
+ message.append("该技师当前有进行中的订单,时间冲突。\n");
|
|
|
|
|
+
|
|
|
|
|
+ // 现有订单信息
|
|
|
|
|
+ message.append("现有订单:").append(existingOrder.getOrderNo()).append("\n");
|
|
|
|
|
+ if (existingOrder.getStartTime() != null) {
|
|
|
|
|
+ message.append(" 开始时间:").append(existingOrder.getStartTime().format(formatter)).append("\n");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (existingOrder.getEndTime() != null) {
|
|
|
|
|
+ message.append(" 结束时间:").append(existingOrder.getEndTime().format(formatter)).append("\n");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String statusDesc = OrderStatusEnum.getDescByCode(existingOrder.getnStatus());
|
|
|
|
|
+ message.append(" 订单状态:").append(statusDesc).append("\n");
|
|
|
|
|
+
|
|
|
|
|
+ // 新订单信息
|
|
|
|
|
+ message.append("新订单:").append(newOrder.getOrderNo()).append("\n");
|
|
|
|
|
+ if (newOrder.getStartTime() != null) {
|
|
|
|
|
+ message.append(" 预计开始时间:").append(newOrder.getStartTime().format(formatter)).append("\n");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (newOrder.getEndTime() != null) {
|
|
|
|
|
+ message.append(" 预计结束时间:").append(newOrder.getEndTime().format(formatter)).append("\n");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 提示用户
|
|
|
|
|
+ Long durationMinutes = OrderTimeRangeUtils.calculateOrderDuration(newOrder);
|
|
|
|
|
+ if (durationMinutes != null) {
|
|
|
|
|
+ message.append("\n建议:请等待当前订单完成后再试,或选择其他时间段(需间隔至少30分钟)。");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return message.toString();
|
|
|
|
|
+ }
|
|
|
|
|
+}
|