liujie
2026-09-11 4f6f15b818704079ff2e4e4719b798f48d68a335
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
package com.tourguide.modules.order.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.order.dto.TourOrderResponse;
import com.tourguide.modules.order.dto.TourOrderRefundResponse;
import com.tourguide.modules.order.domain.OrderStatus;
import com.tourguide.modules.order.domain.RefundAmountCalculator;
import com.tourguide.modules.order.domain.RefundStatus;
import com.tourguide.modules.order.entity.TourOrder;
import com.tourguide.modules.order.entity.TourOrderRefund;
import com.tourguide.modules.order.mapper.TourOrderMapper;
import com.tourguide.modules.order.mapper.TourOrderRefundMapper;
import com.tourguide.modules.system.service.GuideNotificationService;
import com.tourguide.modules.review.entity.OrderReview;
import com.tourguide.modules.review.mapper.OrderReviewMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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.List;
import java.util.stream.Collectors;
 
@Service
public class TourOrderService {
    private static final Logger log = LoggerFactory.getLogger(TourOrderService.class);
 
    private final TourOrderMapper orderMapper;
    private final OrderReviewMapper reviewMapper;
    private final GuideNotificationService notificationService;
    private final OrderRefundService orderRefundService;
    private final TourOrderRefundMapper refundMapper;
    private final GuideSessionTransitionService sessionTransitionService;
 
    public TourOrderService(TourOrderMapper orderMapper, OrderReviewMapper reviewMapper,
                            GuideNotificationService notificationService,
                            OrderRefundService orderRefundService) {
        this(orderMapper, reviewMapper, notificationService, orderRefundService, null, null);
    }
 
    public TourOrderService(TourOrderMapper orderMapper, OrderReviewMapper reviewMapper,
                            GuideNotificationService notificationService,
                            OrderRefundService orderRefundService,
                            TourOrderRefundMapper refundMapper) {
        this(orderMapper, reviewMapper, notificationService, orderRefundService, refundMapper, null);
    }
 
    @Autowired
    public TourOrderService(TourOrderMapper orderMapper, OrderReviewMapper reviewMapper,
                            GuideNotificationService notificationService,
                            OrderRefundService orderRefundService,
                            TourOrderRefundMapper refundMapper,
                            GuideSessionTransitionService sessionTransitionService) {
        this.orderMapper = orderMapper;
        this.reviewMapper = reviewMapper;
        this.notificationService = notificationService;
        this.orderRefundService = orderRefundService;
        this.refundMapper = refundMapper;
        this.sessionTransitionService = sessionTransitionService;
    }
 
    public PageResponse<TourOrderResponse> page(long page, long size, String keyword, Long visitorId, Long guideId,
                                                String scenicName, String status,
                                                LocalDate startDate, LocalDate endDate) {
        LambdaQueryWrapper<TourOrder> query = new LambdaQueryWrapper<TourOrder>();
        if (StringUtils.hasText(keyword)) {
            String searchKeyword = keyword.trim();
            query.and(wrapper -> wrapper.like(TourOrder::getOrderNo, searchKeyword)
                    .or().like(TourOrder::getVisitorNickname, searchKeyword)
                    .or().like(TourOrder::getVisitorMobile, searchKeyword)
                    .or().like(TourOrder::getGuideName, searchKeyword)
                    .or().like(TourOrder::getGuideMobile, searchKeyword));
        }
        query.eq(visitorId != null, TourOrder::getVisitorId, visitorId)
                .eq(guideId != null, TourOrder::getGuideId, guideId)
                .eq(StringUtils.hasText(scenicName), TourOrder::getScenicName, scenicName)
                .eq(StringUtils.hasText(status), TourOrder::getStatus, status)
                .ge(startDate != null, TourOrder::getCreatedAt, startDate == null ? null : startDate.atStartOfDay())
                .lt(endDate != null, TourOrder::getCreatedAt, endDate == null ? null : endDate.plusDays(1).atStartOfDay())
                .orderByDesc(TourOrder::getId);
        Page<TourOrder> result = orderMapper.selectPage(new Page<TourOrder>(page, size), query);
        return new PageResponse<TourOrderResponse>(
                result.getRecords().stream().map(TourOrderResponse::from).collect(Collectors.toList()),
                result.getTotal(), result.getCurrent(), result.getSize());
    }
 
    public TourOrderResponse detail(Long id) {
        TourOrder order = requireOrder(id);
        OrderReview review = reviewMapper.selectOne(new LambdaQueryWrapper<OrderReview>()
                .eq(OrderReview::getOrderId, id)
                .eq(OrderReview::getStatus, 1)
                .last("LIMIT 1"));
        TourOrderResponse response = TourOrderResponse.from(order, review);
        if (refundMapper == null) return response;
        List<TourOrderRefund> records = refundMapper.selectByOrderId(id);
        if (records == null) return response;
        List<TourOrderRefundResponse> refundRecords = records.stream()
                .map(TourOrderRefundResponse::new)
                .collect(Collectors.toList());
        return response.withRefundRecords(refundRecords);
    }
 
    public List<String> scenicOptions() {
        return orderMapper.selectScenicOptions();
    }
 
    @Transactional(noRollbackFor = BusinessException.class)
    public TourOrderResponse refund(Long id, BigDecimal refundAmount) {
        TourOrder order = requireOrderForUpdate(id);
        boolean notifyGuide = OrderStatus.PENDING_MEETING.matches(order.getStatus()) && order.getGuideId() != null;
        if (OrderStatus.REFUNDED.matches(order.getStatus()) || RefundStatus.REFUNDED.matches(order.getRefundStatus())) {
            throw new BusinessException(409, "订单已退款,请勿重复操作");
        }
        if (order.getPaymentTime() == null) {
            throw new BusinessException(409, "未支付订单不能退款");
        }
        if (RefundStatus.REFUNDING.matches(order.getRefundStatus())) {
            throw new BusinessException(409, "退款正在处理中,请勿重复操作");
        }
        if (OrderStatus.CANCELLED.matches(order.getStatus())
                && !RefundStatus.REFUND_FAILED.matches(order.getRefundStatus())
                && !RefundStatus.PARTIAL.matches(order.getRefundStatus())) {
            throw new BusinessException(409, "已取消订单不能退款");
        }
        boolean followUpRefund = OrderStatus.CANCELLED.matches(order.getStatus())
                && (RefundStatus.REFUND_FAILED.matches(order.getRefundStatus())
                || RefundStatus.PARTIAL.matches(order.getRefundStatus()));
        if (!OrderStatus.isRefundable(order.getStatus()) && !followUpRefund) {
            throw new BusinessException(409, "当前订单状态不支持退款");
        }
        BigDecimal refundableAmount = RefundAmountCalculator.refundableAmount(order);
        if (refundableAmount == null || refundableAmount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new BusinessException(409, "订单无可退金额");
        }
        if (refundAmount == null || refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new BusinessException(400, "退款金额必须大于0");
        }
        if (refundAmount.compareTo(refundableAmount) > 0) {
            throw new BusinessException(400, "退款金额不能超过当前可退金额");
        }
        if (!orderRefundService.requestRefund(order, refundAmount)) {
            throw new BusinessException(502, "支付渠道退款失败,请稍后重试");
        }
        if (notifyGuide) {
            if (sessionTransitionService != null) {
                sessionTransitionService.restoreAfterOrderCancellation(order);
            }
            try {
                notificationService.notifyOrderCancelled(order);
            } catch (RuntimeException exception) {
                log.warn("Failed to notify guide after refund submission, orderId={}, guideId={}",
                        order.getId(), order.getGuideId(), exception);
            }
        }
        return TourOrderResponse.from(order);
    }
 
    private TourOrder requireOrder(Long id) {
        TourOrder order = orderMapper.selectById(id);
        if (order == null) {
            throw new BusinessException(404, "订单不存在");
        }
        return order;
    }
 
    private TourOrder requireOrderForUpdate(Long id) {
        TourOrder order = orderMapper.selectForUpdate(id);
        if (order == null) {
            throw new BusinessException(404, "订单不存在");
        }
        return order;
    }
 
}