| | |
| | | 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.time.LocalDateTime; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.List; |
| | | import java.util.UUID; |
| | | 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) { |
| | | 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, |
| | | 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>(); |
| | |
| | | .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()) |
| | |
| | | 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); |
| | | 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 |
| | | @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 (OrderStatus.CANCELLED.matches(order.getStatus())) { |
| | | 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, "已取消订单不能退款"); |
| | | } |
| | | if (!OrderStatus.isRefundable(order.getStatus())) { |
| | | 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 = order.getPaymentAmount() == null ? order.getAmount() : order.getPaymentAmount(); |
| | | BigDecimal refundableAmount = RefundAmountCalculator.refundableAmount(order); |
| | | if (refundableAmount == null || refundableAmount.compareTo(BigDecimal.ZERO) <= 0) { |
| | | throw new BusinessException(409, "订单无可退金额"); |
| | | } |
| | |
| | | if (refundAmount.compareTo(refundableAmount) > 0) { |
| | | throw new BusinessException(400, "退款金额不能超过当前可退金额"); |
| | | } |
| | | order.setRefundStatus(RefundStatus.REFUNDED.getCode()); |
| | | order.setStatus(OrderStatus.REFUNDED.getCode()); |
| | | order.setRefundAmount(refundAmount); |
| | | order.setRefundTime(LocalDateTime.now()); |
| | | order.setRefundOrderNo(generateRefundOrderNo()); |
| | | order.setRefundMethod(order.getPaymentMethod()); |
| | | orderMapper.updateById(order); |
| | | if (notifyGuide) notificationService.notifyOrderCancelled(order); |
| | | 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); |
| | | } |
| | | |
| | |
| | | return order; |
| | | } |
| | | |
| | | private String generateRefundOrderNo() { |
| | | String time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); |
| | | return "RF" + time + UUID.randomUUID().toString().replace("-", "").substring(0, 6).toUpperCase(); |
| | | } |
| | | } |