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;
}
}