liujie
2026-09-09 39f6fca28e3f78abe48f08e474ec011abb990b36
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
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.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.mapper.TourOrderMapper;
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.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;
 
    public TourOrderService(TourOrderMapper orderMapper, OrderReviewMapper reviewMapper,
                            GuideNotificationService notificationService,
                            OrderRefundService orderRefundService) {
        this.orderMapper = orderMapper;
        this.reviewMapper = reviewMapper;
        this.notificationService = notificationService;
        this.orderRefundService = orderRefundService;
    }
 
    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"));
        return TourOrderResponse.from(order, review);
    }
 
    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) {
            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;
    }
 
}