package com.tourguide.modules.order.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.tourguide.common.exception.BusinessException; 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.OperationScenicSpotMapper; import com.tourguide.modules.operation.mapper.ScenicMeetingPointMapper; import com.tourguide.modules.operation.mapper.ScenicPricingMapper; import com.tourguide.modules.operation.service.PlatformSettingsService; import com.tourguide.modules.order.domain.OrderStatus; import com.tourguide.modules.order.domain.RefundStatus; import com.tourguide.modules.order.dto.MiniOrderCreateRequest; import com.tourguide.modules.order.dto.TourOrderResponse; 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.user.entity.MiniUser; import com.tourguide.modules.user.entity.GuideUser; import com.tourguide.modules.user.mapper.GuideUserMapper; import com.tourguide.modules.user.service.MiniUserService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.UUID; /** 小程序讲解订单创建及状态流转服务。 */ @Service public class MiniTourOrderCommandService { private final TourOrderMapper orderMapper; private final MiniUserService miniUserService; private final OperationScenicSpotMapper scenicSpotMapper; private final ScenicMeetingPointMapper meetingPointMapper; private final ScenicPricingMapper pricingMapper; private final GuideUserMapper guideUserMapper; private final PlatformSettingsService platformSettingsService; private final GuideOrderMatchingService matchingService; private final GuideNotificationService notificationService; public MiniTourOrderCommandService(TourOrderMapper orderMapper, MiniUserService miniUserService, OperationScenicSpotMapper scenicSpotMapper, ScenicMeetingPointMapper meetingPointMapper, ScenicPricingMapper pricingMapper, GuideUserMapper guideUserMapper, PlatformSettingsService platformSettingsService, GuideOrderMatchingService matchingService, GuideNotificationService notificationService) { this.orderMapper = orderMapper; this.miniUserService = miniUserService; this.scenicSpotMapper = scenicSpotMapper; this.meetingPointMapper = meetingPointMapper; this.pricingMapper = pricingMapper; this.guideUserMapper = guideUserMapper; this.platformSettingsService = platformSettingsService; this.matchingService = matchingService; this.notificationService = notificationService; } @Transactional public TourOrderResponse create(Long visitorId, MiniOrderCreateRequest request) { MiniUser visitor = miniUserService.findEnabledById(visitorId); ensureNoUnfinishedOrder(visitor.getId()); OperationScenicSpot scenicSpot = requireScenicSpot(request.getScenicSpotId()); ScenicMeetingPoint meetingPoint = requireMeetingPoint(request.getMeetingPointId(), scenicSpot.getId()); ScenicPricing pricing = requirePricing(request.getServicePackageId(), scenicSpot.getId()); validatePeopleCount(pricing, request.getPeopleCount()); boolean groupService = pricing.getServiceType().startsWith("GROUP_"); BigDecimal originalUnitPrice = valueOrZero(pricing.getOriginalPrice()); BigDecimal discountUnitPrice = valueOrZero(pricing.getDiscountPrice()); BigDecimal multiplier = groupService ? BigDecimal.valueOf(request.getPeopleCount()) : BigDecimal.ONE; BigDecimal originalAmount = originalUnitPrice.multiply(multiplier); BigDecimal subtotalAmount = discountUnitPrice.multiply(multiplier); BigDecimal discountAmount = originalAmount.subtract(subtotalAmount).max(BigDecimal.ZERO); LocalDateTime now = LocalDateTime.now(); TourOrder order = new TourOrder(); order.setOrderNo(generateOrderNo()); order.setVisitorId(visitor.getId()); order.setVisitorNickname(visitor.getNickname()); order.setVisitorMobile(visitor.getMobile()); order.setScenicSpotId(scenicSpot.getId()); order.setScenicName(scenicSpot.getScenicName()); order.setMeetingPointId(meetingPoint.getId()); order.setMeetingPointName(meetingPoint.getPointName()); order.setPackageName(packageName(pricing.getServiceType())); order.setServicePackageId(pricing.getId()); order.setOrderType(pricing.getServiceType()); order.setPeopleCount(request.getPeopleCount()); order.setUnitPrice(discountUnitPrice); order.setOriginalPrice(originalAmount); order.setDiscountPrice(discountUnitPrice); order.setSubtotalAmount(subtotalAmount); order.setDiscountType(discountAmount.signum() > 0 ? "SCENIC_PRICE" : "NONE"); order.setDiscountAmount(discountAmount); order.setAmount(subtotalAmount); order.setPlacedAt(now); order.setMeetingPointCount(1); order.setStatus(OrderStatus.PENDING_PAYMENT.getCode()); order.setRefundStatus(RefundStatus.NONE.getCode()); orderMapper.insert(order); return TourOrderResponse.from(order); } /** * 确认游客订单支付成功,并将订单流转到等待导游接单状态。 * 当前小程序尚未接入微信支付回调,因此由待支付页面在支付动作成功后调用; * 正式接入微信支付后应由服务端支付回调触发本方法对应的领域逻辑。 */ @Transactional public TourOrderResponse confirmPayment(Long visitorId, Long orderId) { TourOrder order = requireVisitorOrder(visitorId, orderId); if (!OrderStatus.PENDING_PAYMENT.matches(order.getStatus())) { throw new BusinessException(409, "当前订单状态不支持支付"); } LocalDateTime now = LocalDateTime.now(); order.setPaymentMethod("WECHAT"); order.setPaymentOrderNo("LOCAL" + order.getOrderNo()); order.setPaymentAmount(order.getAmount()); order.setPaymentTime(now); order.setStatus(OrderStatus.PENDING_ACCEPT.getCode()); orderMapper.updateById(order); matchingService.match(order.getId()); return TourOrderResponse.from(orderMapper.selectById(order.getId())); } public TourOrderResponse active(Long visitorId) { miniUserService.findEnabledById(visitorId); TourOrder order = orderMapper.selectOne( new LambdaQueryWrapper() .eq(TourOrder::getVisitorId, visitorId) .in(TourOrder::getStatus, OrderStatus.unfinishedStatusCodes()) .orderByDesc(TourOrder::getId) .last("LIMIT 1")); return order == null ? null : TourOrderResponse.from(order); } public TourOrderResponse detail(Long visitorId, Long orderId) { TourOrder order = requireVisitorOrder(visitorId, orderId); OperationScenicSpot scenicSpot = scenicSpotMapper.selectOne( new LambdaQueryWrapper() .eq(OperationScenicSpot::getScenicName, order.getScenicName()) .orderByDesc(OperationScenicSpot::getId) .last("LIMIT 1")); ScenicMeetingPoint meetingPoint = scenicSpot == null ? null : meetingPointMapper.selectOne( new LambdaQueryWrapper() .eq(ScenicMeetingPoint::getScenicSpotId, scenicSpot.getId()) .eq(ScenicMeetingPoint::getPointName, order.getMeetingPointName()) .orderByDesc(ScenicMeetingPoint::getId) .last("LIMIT 1")); ScenicPricing pricing = scenicSpot == null ? null : pricingMapper.selectOne( new LambdaQueryWrapper() .eq(ScenicPricing::getScenicSpotId, scenicSpot.getId()) .eq(ScenicPricing::getServiceType, order.getOrderType()) .orderByDesc(ScenicPricing::getId) .last("LIMIT 1")); GuideUser guide = order.getGuideId() == null ? null : guideUserMapper.selectById(order.getGuideId()); return TourOrderResponse.from(order) .enrich(scenicSpot, meetingPoint, pricing, guide) .withUnacceptedAutoRefundMinutes( platformSettingsService.getSettings().getUnacceptedAutoRefundMinutes()); } @Transactional public TourOrderResponse cancel(Long visitorId, Long orderId) { TourOrder order = requireVisitorOrder(visitorId, orderId); boolean notifyGuide = OrderStatus.PENDING_MEETING.matches(order.getStatus()) && order.getGuideId() != null; if (!OrderStatus.PENDING_PAYMENT.matches(order.getStatus()) && !OrderStatus.PENDING_ACCEPT.matches(order.getStatus()) && !OrderStatus.PENDING_MEETING.matches(order.getStatus())) { throw new BusinessException(409, "当前订单状态不支持取消"); } order.setStatus(OrderStatus.CANCELLED.getCode()); if (order.getPaymentTime() != null) { order.setRefundStatus(RefundStatus.REFUNDED.getCode()); order.setRefundAmount(order.getPaymentAmount()); order.setRefundTime(LocalDateTime.now()); order.setRefundOrderNo("RF" + generateOrderNo()); order.setRefundMethod(order.getPaymentMethod()); } orderMapper.updateById(order); if (notifyGuide) notificationService.notifyOrderCancelled(order); return TourOrderResponse.from(order); } private OperationScenicSpot requireScenicSpot(Long id) { OperationScenicSpot scenicSpot = scenicSpotMapper.selectOne( new LambdaQueryWrapper() .eq(OperationScenicSpot::getId, id) .eq(OperationScenicSpot::getStatus, 1)); if (scenicSpot == null) throw new BusinessException(404, "景区不存在或未启用"); return scenicSpot; } private void ensureNoUnfinishedOrder(Long visitorId) { Long count = orderMapper.selectCount( new LambdaQueryWrapper() .eq(TourOrder::getVisitorId, visitorId) .in(TourOrder::getStatus, OrderStatus.unfinishedStatusCodes())); if (count != null && count > 0) { throw new BusinessException(409, "您有一个未完成的订单,请先完成或取消后再下单"); } } private ScenicMeetingPoint requireMeetingPoint(Long id, Long scenicSpotId) { ScenicMeetingPoint point = meetingPointMapper.selectOne( new LambdaQueryWrapper() .eq(ScenicMeetingPoint::getId, id) .eq(ScenicMeetingPoint::getScenicSpotId, scenicSpotId) .eq(ScenicMeetingPoint::getStatus, 1)); if (point == null) throw new BusinessException(400, "汇合点不属于当前景区或已停用"); return point; } private ScenicPricing requirePricing(Long id, Long scenicSpotId) { ScenicPricing pricing = pricingMapper.selectOne( new LambdaQueryWrapper() .eq(ScenicPricing::getId, id) .eq(ScenicPricing::getScenicSpotId, scenicSpotId)); if (pricing == null) throw new BusinessException(400, "讲解套餐不属于当前景区"); if (pricing.getDiscountPrice() == null || pricing.getDiscountPrice().signum() <= 0) { throw new BusinessException(409, "讲解套餐价格未配置"); } return pricing; } private TourOrder requireVisitorOrder(Long visitorId, Long orderId) { miniUserService.findEnabledById(visitorId); TourOrder order = orderMapper.selectOne(new LambdaQueryWrapper() .eq(TourOrder::getId, orderId) .eq(TourOrder::getVisitorId, visitorId)); if (order == null) throw new BusinessException(404, "订单不存在"); return order; } private void validatePeopleCount(ScenicPricing pricing, Integer peopleCount) { if (pricing.getServiceType().startsWith("GROUP_") && pricing.getMaxParticipants() != null && peopleCount > pricing.getMaxParticipants()) { throw new BusinessException(400, "讲解人数不能超过套餐人数上限"); } } private BigDecimal valueOrZero(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } private String packageName(String serviceType) { if ("GROUP_QUICK".equals(serviceType)) return "拼团 · 快讲"; if ("GROUP_PREMIUM".equals(serviceType)) return "拼团 · 精讲"; if ("PRIVATE_QUICK".equals(serviceType)) return "包团 · 快讲"; if ("PRIVATE_PREMIUM".equals(serviceType)) return "包团 · 精讲"; throw new BusinessException(400, "不支持的讲解套餐类型"); } private String generateOrderNo() { String time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")); String random = UUID.randomUUID().toString().replace("-", "").substring(0, 5).toUpperCase(); return "MJ" + time + random; } }