liujie
2026-09-16 a3a56121a476cd23d0f29bc391c07374dc5008f2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
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.setGrade(scenicSpot.getGrade());
        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.setGrade(scenicSpot.getGrade());
            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.setAddress(request.getAddress().trim());
            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.setGrade(request.getGrade() == null ? "" : request.getGrade().trim());
        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, "景区编号已存在");
        }
    }
}