package com.tourguide.modules.operation.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.tourguide.common.api.PageResponse; import com.tourguide.common.exception.BusinessException; import com.tourguide.modules.operation.domain.ServiceType; import com.tourguide.modules.operation.dto.*; import com.tourguide.modules.operation.entity.OperationCity; 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.OperationCityMapper; import com.tourguide.modules.operation.mapper.OperationScenicSpotMapper; import com.tourguide.modules.operation.mapper.ScenicMeetingPointMapper; import com.tourguide.modules.operation.mapper.ScenicPricingMapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.math.BigDecimal; import java.time.LocalDate; import java.util.*; import java.util.stream.Collectors; /** 景区列表、详情及运营配置服务。 */ @Service public class OperationScenicSpotService { private final OperationScenicSpotMapper scenicSpotMapper; private final OperationCityMapper cityMapper; private final ScenicPricingMapper pricingMapper; private final ScenicMeetingPointMapper meetingPointMapper; public OperationScenicSpotService(OperationScenicSpotMapper scenicSpotMapper, OperationCityMapper cityMapper, ScenicPricingMapper pricingMapper, ScenicMeetingPointMapper meetingPointMapper) { this.scenicSpotMapper = scenicSpotMapper; this.cityMapper = cityMapper; this.pricingMapper = pricingMapper; this.meetingPointMapper = meetingPointMapper; } public PageResponse page(long page, long size, String keyword, Long cityId, Integer status, LocalDate startDate, LocalDate endDate) { LambdaQueryWrapper query = new LambdaQueryWrapper() .like(StringUtils.hasText(keyword), OperationScenicSpot::getScenicName, StringUtils.hasText(keyword) ? keyword.trim() : null) .eq(cityId != null, OperationScenicSpot::getCityId, cityId) .eq(status != null, OperationScenicSpot::getStatus, status) .ge(startDate != null, OperationScenicSpot::getCreatedAt, startDate == null ? null : startDate.atStartOfDay()) .lt(endDate != null, OperationScenicSpot::getCreatedAt, endDate == null ? null : endDate.plusDays(1).atStartOfDay()) .orderByDesc(OperationScenicSpot::getId); Page result = scenicSpotMapper.selectPage(new Page(page, size), query); List records = buildListResponses(result.getRecords()); return new PageResponse(records, result.getTotal(), result.getCurrent(), result.getSize()); } @Transactional public ScenicSpotDetailResponse create(ScenicSpotDetailSaveRequest request) { OperationCity city = requireCity(request.getCityId()); String scenicCode = request.getScenicCode().trim(); String scenicName = request.getScenicName().trim(); ensureUniqueCode(null, scenicCode); ensureUniqueName(null, city.getId(), scenicName); validateDetailRequest(request); OperationScenicSpot scenicSpot = new OperationScenicSpot(); scenicSpot.setStatus(1); scenicSpot.setDeleted(0); applyDetailFields(scenicSpot, city.getId(), scenicCode, scenicName, request); scenicSpotMapper.insert(scenicSpot); insertPricingConfigurations(scenicSpot.getId(), request); replaceMeetingPoints(scenicSpot.getId(), request.getMeetingPoints()); return detail(scenicSpot.getId()); } public ScenicSpotDetailResponse detail(Long id) { OperationScenicSpot scenicSpot = requireScenicSpot(id); OperationCity city = requireCity(scenicSpot.getCityId()); Map pricingMap = pricingMapper.selectList( new LambdaQueryWrapper() .eq(ScenicPricing::getScenicSpotId, id) .orderByAsc(ScenicPricing::getId)) .stream().collect(Collectors.toMap(ScenicPricing::getServiceType, value -> value)); List meetingPoints = meetingPointMapper.selectList( new LambdaQueryWrapper() .eq(ScenicMeetingPoint::getScenicSpotId, id) .orderByAsc(ScenicMeetingPoint::getSortOrder, ScenicMeetingPoint::getId)) .stream().map(ScenicMeetingPointResponse::from).collect(Collectors.toList()); ScenicSpotDetailResponse response = new ScenicSpotDetailResponse(); response.setId(scenicSpot.getId()); response.setScenicCode(scenicSpot.getScenicCode()); response.setScenicName(scenicSpot.getScenicName()); response.setCityId(scenicSpot.getCityId()); response.setCityName(city.getCityName()); response.setCreatedAt(scenicSpot.getCreatedAt()); response.setGroupFast(priceResponse(pricingMap, ServiceType.GROUP_QUICK.getCode())); response.setGroupPremium(priceResponse(pricingMap, ServiceType.GROUP_PREMIUM.getCode())); response.setPrivateFast(priceResponse(pricingMap, ServiceType.PRIVATE_QUICK.getCode())); response.setPrivatePremium(priceResponse(pricingMap, ServiceType.PRIVATE_PREMIUM.getCode())); response.setMeetingPoints(meetingPoints); response.setFirstOrderLeadMinutes(scenicSpot.getFirstOrderLeadMinutes()); response.setVisitorNoOrderMinutes(scenicSpot.getVisitorNoOrderMinutes()); response.setUnpaidCancelMinutes(scenicSpot.getUnpaidCancelMinutes()); return response; } @Transactional public ScenicSpotDetailResponse save(Long id, ScenicSpotDetailSaveRequest request) { OperationScenicSpot scenicSpot = requireScenicSpot(id); OperationCity city = requireCity(request.getCityId()); String scenicCode = request.getScenicCode().trim(); String scenicName = request.getScenicName().trim(); ensureUniqueCode(id, scenicCode); ensureUniqueName(id, city.getId(), scenicName); validateDetailRequest(request); applyDetailFields(scenicSpot, city.getId(), scenicCode, scenicName, request); scenicSpotMapper.updateById(scenicSpot); pricingMapper.markDeletedByScenicSpotId(id); insertPricingConfigurations(id, request); replaceMeetingPoints(id, request.getMeetingPoints()); return detail(id); } @Transactional public void updateStatus(Long id, Integer status) { OperationScenicSpot scenicSpot = requireScenicSpot(id); scenicSpot.setStatus(status); scenicSpotMapper.updateById(scenicSpot); } private List buildListResponses(List scenicSpots) { if (scenicSpots.isEmpty()) { return Collections.emptyList(); } Set cityIds = scenicSpots.stream().map(OperationScenicSpot::getCityId).collect(Collectors.toSet()); Set scenicIds = scenicSpots.stream().map(OperationScenicSpot::getId).collect(Collectors.toSet()); Map cityNames = cityMapper.selectBatchIds(cityIds).stream() .collect(Collectors.toMap(OperationCity::getId, OperationCity::getCityName)); Map> pricingByScenic = new HashMap>(); for (ScenicPricing pricing : pricingMapper.selectList( new LambdaQueryWrapper().in(ScenicPricing::getScenicSpotId, scenicIds))) { pricingByScenic.computeIfAbsent(pricing.getScenicSpotId(), key -> new HashMap()) .put(pricing.getServiceType(), pricing); } List responses = new ArrayList(); for (OperationScenicSpot scenicSpot : scenicSpots) { Map prices = pricingByScenic.getOrDefault( scenicSpot.getId(), Collections.emptyMap()); ScenicSpotListResponse response = new ScenicSpotListResponse(); response.setId(scenicSpot.getId()); response.setScenicCode(scenicSpot.getScenicCode()); response.setScenicName(scenicSpot.getScenicName()); response.setCityId(scenicSpot.getCityId()); response.setCityName(cityNames.get(scenicSpot.getCityId())); response.setGroupFastPrice(discountPrice(prices.get(ServiceType.GROUP_QUICK.getCode()))); response.setGroupPremiumPrice(discountPrice(prices.get(ServiceType.GROUP_PREMIUM.getCode()))); response.setPrivateFastPrice(discountPrice(prices.get(ServiceType.PRIVATE_QUICK.getCode()))); response.setPrivatePremiumPrice(discountPrice(prices.get(ServiceType.PRIVATE_PREMIUM.getCode()))); response.setStatus(scenicSpot.getStatus()); response.setCreatedAt(scenicSpot.getCreatedAt()); responses.add(response); } return responses; } private void replaceMeetingPoints(Long scenicSpotId, List requests) { meetingPointMapper.markDeletedByScenicSpotId(scenicSpotId); if (requests == null || requests.isEmpty()) { return; } long defaultCount = requests.stream().filter(request -> Integer.valueOf(1).equals(request.getIsDefault())).count(); if (defaultCount == 0) { throw new BusinessException(400, "请设置一个默认汇合点"); } if (defaultCount > 1) { throw new BusinessException(400, "同一景区只能配置一个默认汇合点"); } Set pointNames = new HashSet(); for (int index = 0; index < requests.size(); index++) { ScenicMeetingPointRequest request = requests.get(index); String pointName = request.getPointName().trim(); if (!pointNames.add(pointName)) { throw new BusinessException(409, "同一景区不能配置重名汇合点"); } ScenicMeetingPoint point = new ScenicMeetingPoint(); point.setScenicSpotId(scenicSpotId); point.setPointName(pointName); point.setLongitude(request.getLongitude()); point.setLatitude(request.getLatitude()); point.setStatus(request.getStatus()); point.setIsDefault(request.getIsDefault()); point.setSortOrder(index + 1); meetingPointMapper.upsert(point); } } private void insertPricing(Long scenicSpotId, String serviceType, ScenicPriceConfigRequest request) { ScenicPricing pricing = new ScenicPricing(); pricing.setScenicSpotId(scenicSpotId); pricing.setServiceType(serviceType); pricing.setDurationMinutes(request.getDurationMinutes()); pricing.setMinParticipants(request.getMinParticipants()); pricing.setMaxParticipants(request.getMaxParticipants()); pricing.setOriginalPrice(request.getOriginalPrice()); pricing.setDiscountPrice(request.getDiscountPrice()); pricingMapper.upsert(pricing); } private void insertPricingConfigurations(Long scenicSpotId, ScenicSpotDetailSaveRequest request) { insertPricing(scenicSpotId, ServiceType.GROUP_QUICK.getCode(), request.getGroupFast()); insertPricing(scenicSpotId, ServiceType.GROUP_PREMIUM.getCode(), request.getGroupPremium()); insertPricing(scenicSpotId, ServiceType.PRIVATE_QUICK.getCode(), request.getPrivateFast()); insertPricing(scenicSpotId, ServiceType.PRIVATE_PREMIUM.getCode(), request.getPrivatePremium()); } private void validateDetailRequest(ScenicSpotDetailSaveRequest request) { validatePricing(request.getGroupFast(), true); validatePricing(request.getGroupPremium(), true); validatePricing(request.getPrivateFast(), false); validatePricing(request.getPrivatePremium(), false); } private void applyDetailFields(OperationScenicSpot scenicSpot, Long cityId, String scenicCode, String scenicName, ScenicSpotDetailSaveRequest request) { scenicSpot.setScenicCode(scenicCode); scenicSpot.setScenicName(scenicName); scenicSpot.setCityId(cityId); scenicSpot.setFirstOrderLeadMinutes(request.getFirstOrderLeadMinutes()); scenicSpot.setVisitorNoOrderMinutes(request.getVisitorNoOrderMinutes()); scenicSpot.setUnpaidCancelMinutes(request.getUnpaidCancelMinutes()); } private void validatePricing(ScenicPriceConfigRequest request, boolean groupService) { if (groupService && request.getMaxParticipants() == null) { throw new BusinessException(400, "拼团讲解必须配置最多讲解人数"); } if (request.getMaxParticipants() != null && request.getMaxParticipants() < request.getMinParticipants()) { throw new BusinessException(400, "最多讲解人数不能小于最少讲解人数"); } if (request.getDiscountPrice().compareTo(request.getOriginalPrice()) > 0) { throw new BusinessException(400, "优惠价不能高于原价"); } if (!groupService) { request.setMaxParticipants(null); } } private ScenicPriceConfigResponse priceResponse(Map pricingMap, String serviceType) { ScenicPricing pricing = pricingMap.get(serviceType); if (pricing == null) { throw new BusinessException(500, "景区价格配置不完整"); } return ScenicPriceConfigResponse.from(pricing); } private BigDecimal discountPrice(ScenicPricing pricing) { return pricing == null ? BigDecimal.ZERO : pricing.getDiscountPrice(); } private OperationScenicSpot requireScenicSpot(Long id) { OperationScenicSpot scenicSpot = scenicSpotMapper.selectById(id); if (scenicSpot == null) { throw new BusinessException(404, "景区不存在"); } return scenicSpot; } private OperationCity requireCity(Long id) { OperationCity city = cityMapper.selectById(id); if (city == null) { throw new BusinessException(404, "所属城市不存在"); } return city; } private void ensureUniqueName(Long id, Long cityId, String scenicName) { LambdaQueryWrapper query = new LambdaQueryWrapper() .eq(OperationScenicSpot::getCityId, cityId) .eq(OperationScenicSpot::getScenicName, scenicName) .ne(id != null, OperationScenicSpot::getId, id); if (scenicSpotMapper.selectCount(query) > 0) { throw new BusinessException(409, "该城市下已存在同名景区"); } } private void ensureUniqueCode(Long id, String scenicCode) { LambdaQueryWrapper query = new LambdaQueryWrapper() .eq(OperationScenicSpot::getScenicCode, scenicCode) .ne(id != null, OperationScenicSpot::getId, id); if (scenicSpotMapper.selectCount(query) > 0) { throw new BusinessException(409, "景区编号已存在"); } } }