package com.tourguide.modules.order.service; import com.tourguide.modules.operation.entity.OperationScenicSpot; import com.tourguide.modules.operation.entity.ScenicPricing; import com.tourguide.modules.operation.mapper.OperationScenicSpotMapper; import com.tourguide.modules.operation.mapper.ScenicPricingMapper; import com.tourguide.modules.order.domain.OrderStatus; import com.tourguide.modules.order.entity.TourOrder; import com.tourguide.modules.order.mapper.TourOrderMapper; import com.tourguide.modules.system.service.GuideNotificationService; import com.tourguide.modules.user.entity.GuideReceivingSession; import com.tourguide.modules.user.entity.GuideUser; import com.tourguide.modules.user.mapper.GuideRealtimeStatusMapper; import com.tourguide.modules.user.mapper.GuideReceivingSessionMapper; import com.tourguide.modules.user.mapper.GuideUserMapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.List; @Service public class GuideOrderMatchingService { private final TourOrderMapper orderMapper; private final GuideReceivingSessionMapper sessionMapper; private final GuideUserMapper guideUserMapper; private final GuideRealtimeStatusMapper realtimeStatusMapper; private final OperationScenicSpotMapper scenicMapper; private final ScenicPricingMapper pricingMapper; private final GuideOrderEarningSnapshotService earningSnapshotService; private final GuideNotificationService notificationService; public GuideOrderMatchingService(TourOrderMapper orderMapper, GuideReceivingSessionMapper sessionMapper, GuideUserMapper guideUserMapper, GuideRealtimeStatusMapper realtimeStatusMapper, OperationScenicSpotMapper scenicMapper, ScenicPricingMapper pricingMapper, GuideOrderEarningSnapshotService earningSnapshotService, GuideNotificationService notificationService) { this.orderMapper = orderMapper; this.sessionMapper = sessionMapper; this.guideUserMapper = guideUserMapper; this.realtimeStatusMapper = realtimeStatusMapper; this.scenicMapper = scenicMapper; this.pricingMapper = pricingMapper; this.earningSnapshotService = earningSnapshotService; this.notificationService = notificationService; } @Transactional public boolean match(Long orderId) { TourOrder order = orderMapper.selectForUpdate(orderId); if (order == null || !OrderStatus.PENDING_ACCEPT.matches(order.getStatus()) || order.getGuideId() != null) return false; if (order.getScenicSpotId() == null || order.getMeetingPointId() == null || order.getServicePackageId() == null) return false; String mode = order.getOrderType().startsWith("GROUP_") ? "GROUP" : "PRIVATE"; List candidates = sessionMapper.selectMatchCandidates(order.getScenicSpotId(), order.getMeetingPointId(), mode, order.getOrderType()); ScenicPricing pricing = pricingMapper.selectById(order.getServicePackageId()); OperationScenicSpot scenic = scenicMapper.selectById(order.getScenicSpotId()); if (pricing == null || scenic == null) return false; for (GuideReceivingSession session : candidates) { LocalDateTime now = LocalDateTime.now(); boolean reserved = "GROUP".equals(mode) ? reserveGroup(session, order, pricing, scenic, now) : sessionMapper.assignPrivate(session.getId(), order.getPeopleCount(), now) == 1; if (!reserved) continue; GuideUser guide = guideUserMapper.selectById(session.getGuideId()); if (guide == null) continue; order.setReceivingSessionId(session.getId()); order.setGuideId(guide.getId()); order.setGuideName(guide.getRealName()); order.setGuideMobile(guide.getMobile()); order.setAcceptedAt(now); order.setStatus(OrderStatus.PENDING_MEETING.getCode()); earningSnapshotService.apply(order, guide); orderMapper.updateById(order); guideUserMapper.incrementAcceptedOrderCount(guide.getId()); if ("PRIVATE".equals(mode)) realtimeStatusMapper.disableReceiving(guide.getId()); notificationService.notifyOrderAssigned(order); return true; } return false; } private boolean reserveGroup(GuideReceivingSession session, TourOrder order, ScenicPricing pricing, OperationScenicSpot scenic, LocalDateTime now) { Integer maxPeople = pricing.getMaxParticipants(); if (maxPeople == null || maxPeople < 1) return false; int leadMinutes = positive(scenic.getFirstOrderLeadMinutes(), 15); int cutoffMinutes = positive(scenic.getVisitorNoOrderMinutes(), leadMinutes); return sessionMapper.reserveGroupCapacity(session.getId(), order.getPeopleCount(), maxPeople, now, now.plusMinutes(leadMinutes), now.plusMinutes(cutoffMinutes)) == 1; } private int positive(Integer value, int fallback) { return value == null || value < 1 ? fallback : value; } }