PaymentService.java 14.6 KB
package com.ecommerce.payment.service;

import com.ecommerce.payment.client.OrderClient;
import com.ecommerce.payment.model.Payment;
import com.ecommerce.payment.model.dto.PaymentRequest;
import com.ecommerce.payment.model.dto.PaymentResponse;
import com.ecommerce.payment.repository.PaymentRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class PaymentService {
    
    private final PaymentRepository paymentRepository;
    private final StripeService stripeService;
    private final PayPalService payPalService;
    private final RabbitMQService rabbitMQService;
    private final OrderClient orderClient;
    
    @Transactional
    @Caching(evict = {
        @CacheEvict(value = "payments", allEntries = true),
        @CacheEvict(value = "payment", key = "#result.paymentId")
    })
    public PaymentResponse createPayment(PaymentRequest request) {
        // Validate order exists and amount matches
        validateOrder(request);
        
        // Create payment record
        Payment payment = new Payment();
        mapPaymentRequestToEntity(request, payment);
        
        Payment savedPayment = paymentRepository.save(payment);
        
        // Process payment based on payment method
        PaymentResponse response = processPayment(savedPayment, request);
        
        log.info("Payment created: {}", savedPayment.getPaymentId());
        return response;
    }
    
    @Cacheable(value = "payment", key = "#paymentId")
    public PaymentResponse getPayment(String paymentId) {
        Payment payment = paymentRepository.findByPaymentId(paymentId)
                .orElseThrow(() -> new RuntimeException("Payment not found: " + paymentId));
        return mapPaymentToResponse(payment);
    }
    
    @Cacheable(value = "payment", key = "#gatewayPaymentId")
    public PaymentResponse getPaymentByGatewayId(String gatewayPaymentId) {
        Payment payment = paymentRepository.findByGatewayPaymentId(gatewayPaymentId)
                .orElseThrow(() -> new RuntimeException("Payment not found with gateway ID: " + gatewayPaymentId));
        return mapPaymentToResponse(payment);
    }
    
    @Cacheable(value = "payments", key = "#orderId")
    public List<PaymentResponse> getPaymentsByOrder(String orderId) {
        List<Payment> payments = paymentRepository.findByOrderId(orderId);
        return payments.stream()
                .map(this::mapPaymentToResponse)
                .collect(Collectors.toList());
    }
    
    @Cacheable(value = "payments", key = "#userId + '-' + #pageable.pageNumber")
    public Page<PaymentResponse> getPaymentsByUser(Long userId, Pageable pageable) {
        return paymentRepository.findByUserId(userId, pageable)
                .map(this::mapPaymentToResponse);
    }
    
    @Cacheable(value = "payments", key = "#status + '-' + #pageable.pageNumber")
    public Page<PaymentResponse> getPaymentsByStatus(String status, Pageable pageable) {
        return paymentRepository.findByStatus(status, pageable)
                .map(this::mapPaymentToResponse);
    }
    
    @Cacheable(value = "payments", key = "#pageable.pageNumber + '-' + #pageable.pageSize")
    public Page<PaymentResponse> getAllPayments(Pageable pageable) {
        return paymentRepository.findAll(pageable)
                .map(this::mapPaymentToResponse);
    }
    
    @Transactional
    @Caching(evict = {
        @CacheEvict(value = "payments", allEntries = true),
        @CacheEvict(value = "payment", key = "#paymentId")
    })
    public PaymentResponse confirmPayment(String paymentId) {
        Payment payment = paymentRepository.findByPaymentId(paymentId)
                .orElseThrow(() -> new RuntimeException("Payment not found: " + paymentId));
        
        if (!"PROCESSING".equals(payment.getStatus())) {
            throw new RuntimeException("Payment is not in PROCESSING status");
        }
        
        // Confirm payment with gateway
        PaymentResponse response = confirmPaymentWithGateway(payment);
        
        // Update order status if payment succeeded
        if ("SUCCEEDED".equals(response.getStatus())) {
            updateOrderStatus(payment.getOrderNumber(), "CONFIRMED", "Payment confirmed");
            rabbitMQService.sendPaymentSuccessEvent(payment);
        }
        
        log.info("Payment confirmed: {} -> {}", paymentId, response.getStatus());
        return response;
    }
    
    @Transactional
    @Caching(evict = {
        @CacheEvict(value = "payments", allEntries = true),
        @CacheEvict(value = "payment", key = "#paymentId")
    })
    public PaymentResponse cancelPayment(String paymentId, String reason) {
        Payment payment = paymentRepository.findByPaymentId(paymentId)
                .orElseThrow(() -> new RuntimeException("Payment not found: " + paymentId));
        
        if (!"PENDING".equals(payment.getStatus()) && !"PROCESSING".equals(payment.getStatus())) {
            throw new RuntimeException("Payment cannot be cancelled in current status: " + payment.getStatus());
        }
        
        payment.setStatus("CANCELLED");
        payment.setFailureReason(reason);
        payment.setUpdatedAt(LocalDateTime.now());
        
        Payment cancelledPayment = paymentRepository.save(payment);
        
        rabbitMQService.sendPaymentCancelledEvent(cancelledPayment);
        log.info("Payment cancelled: {}", paymentId);
        
        return mapPaymentToResponse(cancelledPayment);
    }
    
    public Map<String, Object> getPaymentStatistics(LocalDateTime startDate, LocalDateTime endDate) {
        BigDecimal totalRevenue = paymentRepository.getTotalRevenueByDateRange(startDate, endDate);
        Long pendingCount = paymentRepository.countByStatus("PENDING");
        Long succeededCount = paymentRepository.countByStatus("SUCCEEDED");
        Long failedCount = paymentRepository.countByStatus("FAILED");
        
        List<Object[]> methodStats = paymentRepository.getPaymentMethodStats();
        Map<String, Long> paymentMethodCounts = new HashMap<>();
        for (Object[] stat : methodStats) {
            paymentMethodCounts.put((String) stat[0], (Long) stat[1]);
        }
        
        Map<String, Object> stats = new HashMap<>();
        stats.put("totalRevenue", totalRevenue != null ? totalRevenue : BigDecimal.ZERO);
        stats.put("pendingPayments", pendingCount != null ? pendingCount : 0L);
        stats.put("succeededPayments", succeededCount != null ? succeededCount : 0L);
        stats.put("failedPayments", failedCount != null ? failedCount : 0L);
        stats.put("paymentMethodStats", paymentMethodCounts);
        stats.put("startDate", startDate);
        stats.put("endDate", endDate);
        
        return stats;
    }
    
    private void validateOrder(PaymentRequest request) {
        try {
            Map<String, Object> order = orderClient.getOrderByNumber(request.getOrderNumber());
            
            if (order == null) {
                throw new RuntimeException("Order not found: " + request.getOrderNumber());
            }
            
            BigDecimal orderAmount = new BigDecimal(order.get("totalAmount").toString());
            if (request.getAmount().compareTo(orderAmount) != 0) {
                throw new RuntimeException("Payment amount does not match order total");
            }
            
            String orderStatus = (String) order.get("status");
            if (!"PENDING".equals(orderStatus)) {
                throw new RuntimeException("Order is not in PENDING status");
            }
            
        } catch (Exception e) {
            throw new RuntimeException("Order validation failed: " + e.getMessage());
        }
    }
    
    private PaymentResponse processPayment(Payment payment, PaymentRequest request) {
        try {
            PaymentResponse response;
            
            switch (payment.getPaymentMethod().toUpperCase()) {
                case "STRIPE":
                    response = stripeService.createPaymentIntent(payment, request);
                    break;
                case "PAYPAL":
                    response = payPalService.createPayment(payment, request);
                    break;
                case "CREDIT_CARD":
                    // Direct credit card processing
                    response = processCreditCardPayment(payment, request);
                    break;
                default:
                    throw new RuntimeException("Unsupported payment method: " + payment.getPaymentMethod());
            }
            
            // Update payment with gateway response
            updatePaymentFromResponse(payment, response);
            paymentRepository.save(payment);
            
            return response;
            
        } catch (Exception e) {
            // Update payment as failed
            payment.setStatus("FAILED");
            payment.setFailureReason(e.getMessage());
            payment.setUpdatedAt(LocalDateTime.now());
            paymentRepository.save(payment);
            
            throw new RuntimeException("Payment processing failed: " + e.getMessage());
        }
    }
    
    private PaymentResponse confirmPaymentWithGateway(Payment payment) {
        try {
            PaymentResponse response;
            
            switch (payment.getPaymentGateway().toUpperCase()) {
                case "STRIPE":
                    response = stripeService.confirmPayment(payment);
                    break;
                case "PAYPAL":
                    response = payPalService.confirmPayment(payment);
                    break;
                default:
                    throw new RuntimeException("Unsupported payment gateway: " + payment.getPaymentGateway());
            }
            
            // Update payment with confirmation response
            updatePaymentFromResponse(payment, response);
            
            if ("SUCCEEDED".equals(response.getStatus())) {
                payment.setProcessedAt(LocalDateTime.now());
            }
            
            paymentRepository.save(payment);
            return response;
            
        } catch (Exception e) {
            payment.setStatus("FAILED");
            payment.setFailureReason(e.getMessage());
            payment.setUpdatedAt(LocalDateTime.now());
            paymentRepository.save(payment);
            
            throw new RuntimeException("Payment confirmation failed: " + e.getMessage());
        }
    }
    
    private PaymentResponse processCreditCardPayment(Payment payment, PaymentRequest request) {
        // Simulate credit card processing
        // In production, integrate with a payment processor like Stripe, Braintree, etc.
        
        boolean success = Math.random() > 0.1; // 90% success rate for demo
        
        PaymentResponse response = new PaymentResponse();
        response.setPaymentId(payment.getPaymentId());
        response.setStatus(success ? "SUCCEEDED" : "FAILED");
        response.setGatewayPaymentId("CC_" + System.currentTimeMillis());
        
        if (!success) {
            response.setFailureReason("Credit card processing failed");
            response.setFailureCode("CARD_DECLINED");
        }
        
        return response;
    }
    
    private void updatePaymentFromResponse(Payment payment, PaymentResponse response) {
        payment.setStatus(response.getStatus());
        payment.setGatewayPaymentId(response.getGatewayPaymentId());
        payment.setFailureReason(response.getFailureReason());
        payment.setFailureCode(response.getFailureCode());
        payment.setUpdatedAt(LocalDateTime.now());
        
        if ("SUCCEEDED".equals(response.getStatus())) {
            payment.setProcessedAt(LocalDateTime.now());
        }
    }
    
    private void updateOrderStatus(String orderNumber, String status, String notes) {
        try {
            Map<String, String> request = new HashMap<>();
            request.put("status", status);
            request.put("notes", notes);
            orderClient.updateOrderStatus(orderNumber, request);
        } catch (Exception e) {
            log.error("Failed to update order status: {}", orderNumber, e);
        }
    }
    
    private void mapPaymentRequestToEntity(PaymentRequest request, Payment payment) {
        // 注意:paymentId 会在 @PrePersist 中自动生成
        payment.setOrderId(request.getOrderId());
        payment.setOrderNumber(request.getOrderNumber());
        payment.setUserId(request.getUserId());
        payment.setAmount(request.getAmount());
        payment.setCurrency(request.getCurrency());
        payment.setPaymentMethod(request.getPaymentMethod());
        payment.setPaymentGateway(determinePaymentGateway(request.getPaymentMethod()));
        payment.setDescription(request.getDescription());
        // 设置初始状态
        payment.setStatus("PENDING");
    }
    
    private String determinePaymentGateway(String paymentMethod) {
        switch (paymentMethod.toUpperCase()) {
            case "STRIPE":
                return "STRIPE";
            case "PAYPAL":
                return "PAYPAL";
            case "CREDIT_CARD":
                return "DIRECT";
            default:
                return "UNKNOWN";
        }
    }
    
    private PaymentResponse mapPaymentToResponse(Payment payment) {
        PaymentResponse response = new PaymentResponse();
        response.setPaymentId(payment.getPaymentId());
        response.setOrderId(payment.getOrderId());
        response.setOrderNumber(payment.getOrderNumber());
        response.setUserId(payment.getUserId());
        response.setAmount(payment.getAmount());
        response.setCurrency(payment.getCurrency());
        response.setStatus(payment.getStatus());
        response.setPaymentMethod(payment.getPaymentMethod());
        response.setPaymentGateway(payment.getPaymentGateway());
        response.setGatewayPaymentId(payment.getGatewayPaymentId());
        response.setDescription(payment.getDescription());
        response.setFailureReason(payment.getFailureReason());
        response.setFailureCode(payment.getFailureCode());
        response.setCreatedAt(payment.getCreatedAt());
        response.setUpdatedAt(payment.getUpdatedAt());
        response.setProcessedAt(payment.getProcessedAt());
        return response;
    }
}