StripeWebhookController.java

package com.yumu.noveltranslator.adapter.in.webhook;

import com.stripe.exception.SignatureVerificationException;
import com.stripe.model.Event;
import com.stripe.net.Webhook;
import com.yumu.noveltranslator.config.tenant.TenantContext;
import com.yumu.noveltranslator.port.in.SubscriptionPort;
import com.yumu.noveltranslator.port.in.WebhookPort;
import com.yumu.noveltranslator.port.out.UserRepositoryPort;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

/**
 * Stripe Webhook 接收端点
 * 接收 Stripe 的订阅状态变更通知,异步处理数据库更新。
 */
@Slf4j
@RestController
@RequestMapping("/webhook")
@RequiredArgsConstructor
public class StripeWebhookController implements WebhookPort {

    private final SubscriptionPort subscriptionPort;
    private final com.yumu.noveltranslator.properties.StripeProperties stripeProperties;
    private final UserRepositoryPort userRepositoryPort;

    @PostMapping("/stripe")
    public String handleStripeWebhook(HttpServletRequest request, @RequestBody String payload) {
        String sigHeader = request.getHeader("Stripe-Signature");

        // 1. 验证签名
        Event event;
        try {
            event = Webhook.constructEvent(payload, sigHeader, stripeProperties.getWebhookSecret());
        } catch (SignatureVerificationException e) {
            log.error("Stripe webhook signature verification failed", e);
            throw new RuntimeException("Invalid webhook signature", e);
        }

        return processEvent(event);
    }

    /**
     * 处理 Stripe 事件(提取为独立方法以便测试)
     */
    public String processEvent(Event event) {
        log.info("Received Stripe webhook event: {}", event.getType());

        // 1. 获取 userId 并设置租户上下文
        Long userId = extractUserId(event);
        if (userId != null) {
            var user = userRepositoryPort.findById(userId).orElse(null);
            if (user != null) {
                TenantContext.setTenantIdOrDefault(user.getTenantId());
            }
        }

        try {
            // 2. 根据事件类型分发处理
            dispatchEvent(event);
            return "{}";
        } finally {
            TenantContext.clear();
        }
    }

    /**
     * 根据事件类型分发到对应的处理方法
     * 包可见性,仅供同包测试使用
     */
    void dispatchEvent(Event event) {
        switch (event.getType()) {
            case "checkout.session.completed" ->
                subscriptionPort.handleCheckoutSessionCompleted(event);

            case "customer.subscription.created" -> {
                // 忽略,避免与 checkout.session.completed 重复处理
                log.info("Ignoring customer.subscription.created event (handled by checkout.session.completed)");
            }

            case "customer.subscription.updated" ->
                subscriptionPort.handleSubscriptionUpdated(event);

            case "customer.subscription.deleted" ->
                subscriptionPort.handleSubscriptionDeleted(event);

            case "customer.subscription.resumed" ->
                subscriptionPort.handleSubscriptionResumed(event);

            case "invoice.payment_succeeded" ->
                subscriptionPort.handleInvoicePaymentSucceeded(event);

            case "invoice.payment_failed" ->
                subscriptionPort.handleInvoicePaymentFailed(event);

            default ->
                log.info("Unhandled Stripe event type: {}", event.getType());
        }
    }

    /**
     * 从事件中提取 userId
     */
    private Long extractUserId(Event event) {
        // 优先从 subscription/session 对象的 metadata 获取
        try {
            var deserializer = event.getDataObjectDeserializer();
            var obj = deserializer.getObject().orElse(null);
            if (obj == null) {
                obj = deserializer.deserializeUnsafe();
            }
            if (obj instanceof com.stripe.model.Subscription sub) {
                if (sub.getMetadata() != null && sub.getMetadata().containsKey("userId")) {
                    return Long.parseLong(sub.getMetadata().get("userId"));
                }
            } else if (obj instanceof com.stripe.model.checkout.Session session) {
                if (session.getMetadata() != null && session.getMetadata().containsKey("userId")) {
                    return Long.parseLong(session.getMetadata().get("userId"));
                }
            } else if (obj instanceof com.stripe.model.Invoice invoice) {
                if (invoice.getMetadata() != null && invoice.getMetadata().containsKey("userId")) {
                    return Long.parseLong(invoice.getMetadata().get("userId"));
                }
            }
        } catch (Exception e) {
            log.warn("Failed to extract userId from event object", e);
        }

        return null;
    }
}