package com.tourguide.modules.order.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.tourguide.config.properties.UniPushProperties; import com.tourguide.modules.operation.entity.OperationScenicSpot; import com.tourguide.modules.operation.mapper.OperationScenicSpotMapper; import com.tourguide.modules.order.domain.OrderStatus; import com.tourguide.modules.order.domain.RefundStatus; 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.mapper.GuideRealtimeStatusMapper; import com.tourguide.modules.user.mapper.GuideReceivingSessionMapper; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.List; @Service public class OrderLifecycleScheduler { private static final int BATCH_SIZE = 100; private final TourOrderMapper orderMapper; private final OperationScenicSpotMapper scenicMapper; private final GuideOrderMatchingService matchingService; private final GuideReceivingSessionMapper sessionMapper; private final GuideSessionTransitionService sessionTransitionService; private final GuideNotificationService notificationService; private final UniPushProperties uniPushProperties; public OrderLifecycleScheduler(TourOrderMapper orderMapper, OperationScenicSpotMapper scenicMapper, GuideOrderMatchingService matchingService, GuideReceivingSessionMapper sessionMapper, GuideSessionTransitionService sessionTransitionService, GuideNotificationService notificationService, UniPushProperties uniPushProperties) { this.orderMapper = orderMapper; this.scenicMapper = scenicMapper; this.matchingService = matchingService; this.sessionMapper = sessionMapper; this.sessionTransitionService = sessionTransitionService; this.notificationService = notificationService; this.uniPushProperties = uniPushProperties; } @Scheduled(fixedDelayString = "${tourguide.order.lifecycle-scan-delay-millis:10000}") public void process() { cancelExpiredUnpaidOrders(); retryPendingOrders(); sendServiceReminders(); startDueGroupSessions(); } void cancelExpiredUnpaidOrders() { List orders = orderMapper.selectList(new LambdaQueryWrapper() .eq(TourOrder::getStatus, OrderStatus.PENDING_PAYMENT.getCode()) .isNotNull(TourOrder::getScenicSpotId).orderByAsc(TourOrder::getPlacedAt).last("LIMIT " + BATCH_SIZE)); LocalDateTime now = LocalDateTime.now(); for (TourOrder order : orders) { OperationScenicSpot scenic = scenicMapper.selectById(order.getScenicSpotId()); int minutes = scenic == null || scenic.getUnpaidCancelMinutes() == null || scenic.getUnpaidCancelMinutes() < 1 ? 15 : scenic.getUnpaidCancelMinutes(); LocalDateTime baseTime = order.getPlacedAt() == null ? order.getCreatedAt() : order.getPlacedAt(); if (baseTime != null && !baseTime.plusMinutes(minutes).isAfter(now)) orderMapper.cancelUnpaid(order.getId()); } } void retryPendingOrders() { List orders = orderMapper.selectList(new LambdaQueryWrapper() .eq(TourOrder::getStatus, OrderStatus.PENDING_ACCEPT.getCode()) .isNotNull(TourOrder::getPaymentTime) .and(wrapper -> wrapper.isNull(TourOrder::getRefundStatus) .or().eq(TourOrder::getRefundStatus, RefundStatus.NONE.getCode())) .orderByAsc(TourOrder::getPaymentTime).last("LIMIT " + BATCH_SIZE)); for (TourOrder order : orders) matchingService.match(order.getId()); } void startDueGroupSessions() { List sessions = sessionMapper.selectList(new LambdaQueryWrapper() .eq(GuideReceivingSession::getStatus, "RECEIVING") .isNotNull(GuideReceivingSession::getServiceStartAt) .le(GuideReceivingSession::getServiceStartAt, LocalDateTime.now()) .orderByAsc(GuideReceivingSession::getServiceStartAt).last("LIMIT " + BATCH_SIZE)); for (GuideReceivingSession session : sessions) { sessionTransitionService.startIfDue(session); } } void sendServiceReminders() { int reminderMinutes = Math.max(1, uniPushProperties.getServiceReminderMinutes()); LocalDateTime now = LocalDateTime.now(); List sessions = sessionMapper.selectList( new LambdaQueryWrapper() .eq(GuideReceivingSession::getStatus, "RECEIVING") .isNull(GuideReceivingSession::getReminderPushedAt) .isNotNull(GuideReceivingSession::getServiceStartAt) .gt(GuideReceivingSession::getServiceStartAt, now) .le(GuideReceivingSession::getServiceStartAt, now.plusMinutes(reminderMinutes)) .orderByAsc(GuideReceivingSession::getServiceStartAt) .last("LIMIT " + BATCH_SIZE)); for (GuideReceivingSession session : sessions) { TourOrder order = orderMapper.selectOne(new LambdaQueryWrapper() .eq(TourOrder::getReceivingSessionId, session.getId()) .eq(TourOrder::getGuideId, session.getGuideId()) .eq(TourOrder::getStatus, OrderStatus.PENDING_MEETING.getCode()) .orderByAsc(TourOrder::getId) .last("LIMIT 1")); if (order == null) continue; LocalDateTime claimedAt = LocalDateTime.now(); if (sessionMapper.claimServiceReminder(session.getId(), claimedAt) != 1) continue; if (!notificationService.pushServiceReminder(order)) { sessionMapper.releaseServiceReminder(session.getId(), claimedAt); } } } }