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<ScenicSpotListResponse> page(long page, long size, String keyword, Long cityId,
|
Integer status, LocalDate startDate, LocalDate endDate) {
|
LambdaQueryWrapper<OperationScenicSpot> query = new LambdaQueryWrapper<OperationScenicSpot>()
|
.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<OperationScenicSpot> result = scenicSpotMapper.selectPage(new Page<OperationScenicSpot>(page, size), query);
|
List<ScenicSpotListResponse> records = buildListResponses(result.getRecords());
|
return new PageResponse<ScenicSpotListResponse>(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<String, ScenicPricing> pricingMap = pricingMapper.selectList(
|
new LambdaQueryWrapper<ScenicPricing>()
|
.eq(ScenicPricing::getScenicSpotId, id)
|
.orderByAsc(ScenicPricing::getId))
|
.stream().collect(Collectors.toMap(ScenicPricing::getServiceType, value -> value));
|
List<ScenicMeetingPointResponse> meetingPoints = meetingPointMapper.selectList(
|
new LambdaQueryWrapper<ScenicMeetingPoint>()
|
.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<ScenicSpotListResponse> buildListResponses(List<OperationScenicSpot> scenicSpots) {
|
if (scenicSpots.isEmpty()) { return Collections.emptyList(); }
|
Set<Long> cityIds = scenicSpots.stream().map(OperationScenicSpot::getCityId).collect(Collectors.toSet());
|
Set<Long> scenicIds = scenicSpots.stream().map(OperationScenicSpot::getId).collect(Collectors.toSet());
|
Map<Long, String> cityNames = cityMapper.selectBatchIds(cityIds).stream()
|
.collect(Collectors.toMap(OperationCity::getId, OperationCity::getCityName));
|
Map<Long, Map<String, ScenicPricing>> pricingByScenic = new HashMap<Long, Map<String, ScenicPricing>>();
|
for (ScenicPricing pricing : pricingMapper.selectList(
|
new LambdaQueryWrapper<ScenicPricing>().in(ScenicPricing::getScenicSpotId, scenicIds))) {
|
pricingByScenic.computeIfAbsent(pricing.getScenicSpotId(), key -> new HashMap<String, ScenicPricing>())
|
.put(pricing.getServiceType(), pricing);
|
}
|
List<ScenicSpotListResponse> responses = new ArrayList<ScenicSpotListResponse>();
|
for (OperationScenicSpot scenicSpot : scenicSpots) {
|
Map<String, ScenicPricing> prices = pricingByScenic.getOrDefault(
|
scenicSpot.getId(), Collections.<String, ScenicPricing>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<ScenicMeetingPointRequest> 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<String> pointNames = new HashSet<String>();
|
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<String, ScenicPricing> 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<OperationScenicSpot> query = new LambdaQueryWrapper<OperationScenicSpot>()
|
.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<OperationScenicSpot> query = new LambdaQueryWrapper<OperationScenicSpot>()
|
.eq(OperationScenicSpot::getScenicCode, scenicCode)
|
.ne(id != null, OperationScenicSpot::getId, id);
|
if (scenicSpotMapper.selectCount(query) > 0) {
|
throw new BusinessException(409, "景区编号已存在");
|
}
|
}
|
}
|