package com.tourguide.modules.user.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.tourguide.common.exception.BusinessException; import com.tourguide.modules.operation.dto.PlatformSettingsResponse; import com.tourguide.modules.operation.entity.OperationScenicSpot; import com.tourguide.modules.operation.entity.ScenicMeetingPoint; import com.tourguide.modules.operation.entity.ScenicPricing; import com.tourguide.modules.operation.mapper.OperationScenicSpotMapper; import com.tourguide.modules.operation.mapper.ScenicMeetingPointMapper; import com.tourguide.modules.operation.mapper.ScenicPricingMapper; import com.tourguide.modules.operation.service.PlatformSettingsService; 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.order.service.GuideSessionTransitionService; import com.tourguide.modules.user.dto.GuideReceivingOptionResponse; import com.tourguide.modules.user.dto.GuideReceivingStartRequest; import com.tourguide.modules.user.entity.GuideRealtimeStatus; import com.tourguide.modules.user.entity.GuideReceivingSession; import com.tourguide.modules.user.entity.GuideScenicConfig; import com.tourguide.modules.user.entity.GuideTeamScenicConfig; 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.GuideScenicConfigMapper; import com.tourguide.modules.user.mapper.GuideTeamScenicConfigMapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @Service public class GuideReceivingService { private final GuideUserService guideUserService; private final GuideScenicConfigMapper guideScenicMapper; private final GuideTeamScenicConfigMapper teamScenicMapper; private final OperationScenicSpotMapper scenicMapper; private final ScenicMeetingPointMapper meetingPointMapper; private final ScenicPricingMapper pricingMapper; private final GuideReceivingSessionMapper sessionMapper; private final GuideRealtimeStatusMapper realtimeStatusMapper; private final PlatformSettingsService platformSettingsService; private final TourOrderMapper orderMapper; private final GuideSessionTransitionService sessionTransitionService; public GuideReceivingService(GuideUserService guideUserService, GuideScenicConfigMapper guideScenicMapper, GuideTeamScenicConfigMapper teamScenicMapper, OperationScenicSpotMapper scenicMapper, ScenicMeetingPointMapper meetingPointMapper, ScenicPricingMapper pricingMapper, GuideReceivingSessionMapper sessionMapper, GuideRealtimeStatusMapper realtimeStatusMapper, PlatformSettingsService platformSettingsService, TourOrderMapper orderMapper, GuideSessionTransitionService sessionTransitionService) { this.guideUserService = guideUserService; this.guideScenicMapper = guideScenicMapper; this.teamScenicMapper = teamScenicMapper; this.scenicMapper = scenicMapper; this.meetingPointMapper = meetingPointMapper; this.pricingMapper = pricingMapper; this.sessionMapper = sessionMapper; this.realtimeStatusMapper = realtimeStatusMapper; this.platformSettingsService = platformSettingsService; this.orderMapper = orderMapper; this.sessionTransitionService = sessionTransitionService; } public GuideReceivingOptionResponse options(Long guideId) { GuideUser guide = guideUserService.findEnabledById(guideId); PlatformSettingsResponse settings = platformSettingsService.getSettings(); boolean premiumEnabled = premiumEnabled(guide, settings); Set scenicNames = configuredScenicNames(guide); if (scenicNames.isEmpty()) { return new GuideReceivingOptionResponse(premiumEnabled, value(settings.getPreciseDispatchOrderCount()), settings.getPreciseDispatchRating(), new ArrayList<>()); } List scenics = scenicMapper.selectList(new LambdaQueryWrapper() .eq(OperationScenicSpot::getStatus, 1).in(OperationScenicSpot::getScenicName, scenicNames) .orderByAsc(OperationScenicSpot::getId)); List rows = new ArrayList<>(); for (OperationScenicSpot scenic : scenics) { List points = meetingPointMapper.selectList( new LambdaQueryWrapper().eq(ScenicMeetingPoint::getScenicSpotId, scenic.getId()) .eq(ScenicMeetingPoint::getStatus, 1).orderByAsc(ScenicMeetingPoint::getSortOrder)) .stream().map(point -> new GuideReceivingOptionResponse.MeetingPointOption(point.getId(), point.getPointName(), point.getLongitude(), point.getLatitude())).collect(Collectors.toList()); List services = pricingMapper.selectList( new LambdaQueryWrapper().eq(ScenicPricing::getScenicSpotId, scenic.getId()) .orderByAsc(ScenicPricing::getId)).stream() .map(item -> new GuideReceivingOptionResponse.ServiceOption(item.getId(), item.getServiceType(), item.getDurationMinutes(), item.getMinParticipants(), item.getMaxParticipants(), item.getOriginalPrice(), item.getDiscountPrice())) .collect(Collectors.toList()); rows.add(new GuideReceivingOptionResponse.ScenicOption(scenic.getId(), scenic.getScenicName(), positive(scenic.getFirstOrderLeadMinutes(), 15), points, services)); } return new GuideReceivingOptionResponse(premiumEnabled, value(settings.getPreciseDispatchOrderCount()), settings.getPreciseDispatchRating(), rows); } @Transactional public GuideReceivingSession start(Long guideId, GuideReceivingStartRequest request) { GuideUser guide = guideUserService.findEnabledById(guideId); String mode = normalizeMode(request.getReceivingMode()); OperationScenicSpot scenic = requireAllowedScenic(guide, request.getScenicSpotId()); requireMeetingPoint(request.getMeetingPointId(), scenic.getId()); Set serviceTypes = new HashSet<>(request.getServiceTypes()); if (serviceTypes.size() != request.getServiceTypes().size()) { throw new BusinessException(400, "service type cannot be duplicated"); } validateServiceTypes(guide, scenic.getId(), mode, serviceTypes); stopActiveSessions(guideId); GuideReceivingSession session = new GuideReceivingSession(); session.setGuideId(guideId); session.setScenicSpotId(scenic.getId()); session.setMeetingPointId(request.getMeetingPointId()); session.setReceivingMode(mode); session.setServiceTypes(String.join(",", serviceTypes)); session.setStatus("RECEIVING"); session.setCurrentPeopleCount(0); sessionMapper.insert(session); saveRealtimeStatus(guideId, request, scenic.getId()); return sessionMapper.selectById(session.getId()); } public GuideReceivingSession current(Long guideId) { guideUserService.findEnabledById(guideId); GuideReceivingSession session = sessionMapper.selectOne(new LambdaQueryWrapper() .eq(GuideReceivingSession::getGuideId, guideId) .in(GuideReceivingSession::getStatus, "RECEIVING", "ASSIGNED", "IN_SERVICE") .orderByDesc(GuideReceivingSession::getId).last("LIMIT 1")); if (session == null) { return null; } if (hasActiveServiceOrder(session)) { return session; } GuideRealtimeStatus realtimeStatus = realtimeStatusMapper.selectById(guideId); if (realtimeStatus == null || !Integer.valueOf(1).equals(realtimeStatus.getOnlineStatus()) || !Integer.valueOf(1).equals(realtimeStatus.getAvailableStatus())) { return null; } return session; } @Transactional public void stop(Long guideId) { guideUserService.findEnabledById(guideId); stopActiveSessions(guideId); realtimeStatusMapper.update(null, new LambdaUpdateWrapper() .eq(GuideRealtimeStatus::getGuideId, guideId) .set(GuideRealtimeStatus::getOnlineStatus, 0) .set(GuideRealtimeStatus::getAvailableStatus, 0)); } @Transactional public void finish(Long guideId) { guideUserService.findEnabledById(guideId); GuideReceivingSession session = sessionMapper.selectOne(new LambdaQueryWrapper() .eq(GuideReceivingSession::getGuideId, guideId) .eq(GuideReceivingSession::getStatus, "IN_SERVICE") .orderByDesc(GuideReceivingSession::getId).last("LIMIT 1")); if (session == null) { throw new BusinessException(400, "no active service session"); } LocalDateTime now = LocalDateTime.now(); if (!sessionTransitionService.completeSession(session, now)) { throw new BusinessException(400, "no active service order"); } } private Set configuredScenicNames(GuideUser guide) { if (guide.getTeamId() == null) { return guideScenicMapper.selectList(new LambdaQueryWrapper() .eq(GuideScenicConfig::getGuideId, guide.getId())).stream() .map(GuideScenicConfig::getScenicName).collect(Collectors.toSet()); } return teamScenicMapper.selectList(new LambdaQueryWrapper() .eq(GuideTeamScenicConfig::getTeamId, guide.getTeamId())).stream() .map(GuideTeamScenicConfig::getScenicName).collect(Collectors.toSet()); } private OperationScenicSpot requireAllowedScenic(GuideUser guide, Long scenicId) { OperationScenicSpot scenic = scenicMapper.selectOne(new LambdaQueryWrapper() .eq(OperationScenicSpot::getId, scenicId).eq(OperationScenicSpot::getStatus, 1)); if (scenic == null) throw new BusinessException(404, "scenic spot is unavailable"); if (!configuredScenicNames(guide).contains(scenic.getScenicName())) { throw new BusinessException(403, "guide is not configured for this scenic spot"); } return scenic; } private void requireMeetingPoint(Long meetingPointId, Long scenicId) { long count = meetingPointMapper.selectCount(new LambdaQueryWrapper() .eq(ScenicMeetingPoint::getId, meetingPointId).eq(ScenicMeetingPoint::getScenicSpotId, scenicId) .eq(ScenicMeetingPoint::getStatus, 1)); if (count == 0) throw new BusinessException(400, "meeting point does not belong to scenic spot"); } private void validateServiceTypes(GuideUser guide, Long scenicId, String mode, Set serviceTypes) { if (serviceTypes.isEmpty()) throw new BusinessException(400, "select at least one service type"); Set configured = pricingMapper.selectList(new LambdaQueryWrapper() .eq(ScenicPricing::getScenicSpotId, scenicId)).stream() .map(ScenicPricing::getServiceType).collect(Collectors.toSet()); for (String type : serviceTypes) { boolean modeMatches = "ALL".equals(mode) ? (type.startsWith("GROUP_") || type.startsWith("PRIVATE_")) : type.startsWith(mode + "_"); if (!modeMatches || !configured.contains(type)) { throw new BusinessException(400, "service type does not match receiving mode or scenic configuration"); } if (type.endsWith("_PREMIUM") && !premiumEnabled(guide, platformSettingsService.getSettings())) { throw new BusinessException(403, "premium service requirements are not met"); } } } private boolean premiumEnabled(GuideUser guide, PlatformSettingsResponse settings) { if (guide == null) { return false; } if (Integer.valueOf(1).equals(guide.getPremiumEnabled()) || Integer.valueOf(1).equals(guide.getAutoDisableWhitelisted())) { return true; } if (settings == null) { return false; } long orderCount = guide.getAcceptedOrderCount() == null ? 0 : guide.getAcceptedOrderCount(); BigDecimal rating = guide.getRating() == null ? BigDecimal.ZERO : guide.getRating(); BigDecimal requiredRating = settings.getPreciseDispatchRating() == null ? BigDecimal.ZERO : settings.getPreciseDispatchRating(); return orderCount >= value(settings.getPreciseDispatchOrderCount()) && rating.compareTo(requiredRating) >= 0; } private void stopActiveSessions(Long guideId) { sessionMapper.update(null, new LambdaUpdateWrapper() .eq(GuideReceivingSession::getGuideId, guideId) .in(GuideReceivingSession::getStatus, "RECEIVING", "ASSIGNED") .set(GuideReceivingSession::getStatus, "STOPPED")); // 订单取消后可能遗留 IN_SERVICE 会话;无未完成订单时才安全关闭。 long activeOrderCount = orderMapper.selectCount(new LambdaQueryWrapper() .eq(TourOrder::getGuideId, guideId) .in(TourOrder::getStatus, OrderStatus.PENDING_ACCEPT.getCode(), OrderStatus.PENDING_MEETING.getCode(), OrderStatus.IN_SERVICE.getCode())); if (activeOrderCount == 0) { sessionMapper.update(null, new LambdaUpdateWrapper() .eq(GuideReceivingSession::getGuideId, guideId) .eq(GuideReceivingSession::getStatus, "IN_SERVICE") .set(GuideReceivingSession::getStatus, "STOPPED")); } } private boolean hasActiveServiceOrder(GuideReceivingSession session) { return orderMapper.selectCount(new LambdaQueryWrapper() .eq(TourOrder::getGuideId, session.getGuideId()) .eq(TourOrder::getReceivingSessionId, session.getId()) .in(TourOrder::getStatus, OrderStatus.PENDING_MEETING.getCode(), OrderStatus.IN_SERVICE.getCode())) > 0; } private void saveRealtimeStatus(Long guideId, GuideReceivingStartRequest request, Long scenicId) { GuideRealtimeStatus status = realtimeStatusMapper.selectById(guideId); boolean insert = status == null; if (insert) { status = new GuideRealtimeStatus(); status.setGuideId(guideId); } status.setScenicSpotId(scenicId); status.setMeetingPointId(request.getMeetingPointId()); status.setLongitude(request.getLongitude()); status.setLatitude(request.getLatitude()); status.setOnlineStatus(1); status.setAvailableStatus(1); status.setLastLocationAt(LocalDateTime.now()); if (insert) realtimeStatusMapper.insert(status); else realtimeStatusMapper.updateById(status); } private String normalizeMode(String mode) { if (!"ALL".equals(mode) && !"GROUP".equals(mode) && !"PRIVATE".equals(mode)) { throw new BusinessException(400, "invalid receiving mode"); } return mode; } private long value(Integer number) { return number == null ? 0L : number.longValue(); } private int positive(Integer number, int fallback) { return number == null || number < 1 ? fallback : number; } }