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 page(long page, long size, String keyword, Long visitorId, Long guideId, String scenicName, String status, LocalDate startDate, LocalDate endDate) { LambdaQueryWrapper query = new LambdaQueryWrapper(); 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 result = orderMapper.selectPage(new Page(page, size), query); return new PageResponse( 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() .eq(OrderReview::getOrderId, id) .eq(OrderReview::getStatus, 1) .last("LIMIT 1")); TourOrderResponse response = TourOrderResponse.from(order, review); if (refundMapper == null) return response; List records = refundMapper.selectByOrderId(id); if (records == null) return response; List refundRecords = records.stream() .map(TourOrderRefundResponse::new) .collect(Collectors.toList()); return response.withRefundRecords(refundRecords); } public List 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; } }