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<String> scenicNames = configuredScenicNames(guide);
|
if (scenicNames.isEmpty()) {
|
return new GuideReceivingOptionResponse(premiumEnabled, value(settings.getPreciseDispatchOrderCount()),
|
settings.getPreciseDispatchRating(), new ArrayList<>());
|
}
|
List<OperationScenicSpot> scenics = scenicMapper.selectList(new LambdaQueryWrapper<OperationScenicSpot>()
|
.eq(OperationScenicSpot::getStatus, 1).in(OperationScenicSpot::getScenicName, scenicNames)
|
.orderByAsc(OperationScenicSpot::getId));
|
List<GuideReceivingOptionResponse.ScenicOption> rows = new ArrayList<>();
|
for (OperationScenicSpot scenic : scenics) {
|
List<GuideReceivingOptionResponse.MeetingPointOption> points = meetingPointMapper.selectList(
|
new LambdaQueryWrapper<ScenicMeetingPoint>().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<GuideReceivingOptionResponse.ServiceOption> services = pricingMapper.selectList(
|
new LambdaQueryWrapper<ScenicPricing>().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<String> 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<GuideReceivingSession>()
|
.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<GuideRealtimeStatus>()
|
.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<GuideReceivingSession>()
|
.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<String> configuredScenicNames(GuideUser guide) {
|
if (guide.getTeamId() == null) {
|
return guideScenicMapper.selectList(new LambdaQueryWrapper<GuideScenicConfig>()
|
.eq(GuideScenicConfig::getGuideId, guide.getId())).stream()
|
.map(GuideScenicConfig::getScenicName).collect(Collectors.toSet());
|
}
|
return teamScenicMapper.selectList(new LambdaQueryWrapper<GuideTeamScenicConfig>()
|
.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<OperationScenicSpot>()
|
.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<ScenicMeetingPoint>()
|
.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<String> serviceTypes) {
|
if (serviceTypes.isEmpty()) throw new BusinessException(400, "select at least one service type");
|
Set<String> configured = pricingMapper.selectList(new LambdaQueryWrapper<ScenicPricing>()
|
.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<GuideReceivingSession>()
|
.eq(GuideReceivingSession::getGuideId, guideId)
|
.in(GuideReceivingSession::getStatus, "RECEIVING", "ASSIGNED")
|
.set(GuideReceivingSession::getStatus, "STOPPED"));
|
|
// 订单取消后可能遗留 IN_SERVICE 会话;无未完成订单时才安全关闭。
|
long activeOrderCount = orderMapper.selectCount(new LambdaQueryWrapper<TourOrder>()
|
.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<GuideReceivingSession>()
|
.eq(GuideReceivingSession::getGuideId, guideId)
|
.eq(GuideReceivingSession::getStatus, "IN_SERVICE")
|
.set(GuideReceivingSession::getStatus, "STOPPED"));
|
}
|
}
|
|
private boolean hasActiveServiceOrder(GuideReceivingSession session) {
|
return orderMapper.selectCount(new LambdaQueryWrapper<TourOrder>()
|
.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;
|
}
|
}
|