知识库首页 模版 creem-webhook-route.ts.md

creem webhook route.ts

本地来源:模版/模版文档对比说明/08-API路由/creem-webhook-route.ts.md

Creem Webhook处理路由 (app/api/webhooks/creem/route.ts) 逐行分析

文件概述

这是处理 Creem 支付系统 Webhook 事件的 API 路由。它负责接收、验证和处理来自 Creem 的各种支付和订阅事件,确保系统状态与 Creem 保持同步。

导入语句分析

import { headers } from "next/headers";
import { NextResponse } from "next/server";
  • headers: Next.js 函数,用于获取请求头信息
  • NextResponse: Next.js 的响应对象,用于构建API响应
import { verifyCreemWebhookSignature } from "@/utils/creem/verify-signature";
import { CreemWebhookEvent } from "@/types/creem";
  • verifyCreemWebhookSignature: 签名验证函数
  • CreemWebhookEvent: Webhook事件类型定义
import {
  createOrUpdateCustomer,
  createOrUpdateSubscription,
  addCreditsToCustomer,
} from "@/utils/supabase/subscriptions";
  • 导入Supabase相关的业务逻辑函数
  • 处理客户、订阅和积分管理

环境变量配置

const CREEM_WEBHOOK_SECRET = process.env.CREEM_WEBHOOK_SECRET!;
  • 获取 Webhook 签名密钥
  • 使用 ! 断言该环境变量存在

主要处理函数分析

export async function POST(request: Request) {
  try {
    const body = await request.text();

    const headersList = headers();
    const signature = (await headersList).get("creem-signature") || "";

    // 验证逻辑...
  } catch (error) {
    console.error("Error processing webhook:", error);
    return new NextResponse("Webhook error", { status: 400 });
  }
}

请求体解析

const body = await request.text();
  • 获取原始请求体文本
  • 保持原始格式用于签名验证

签名验证

const headersList = headers();
const signature = (await headersList).get("creem-signature") || "";

if (
  !signature ||
  !verifyCreemWebhookSignature(body, signature, CREEM_WEBHOOK_SECRET)
) {
  return new NextResponse("Invalid signature", { status: 401 });
}
  • 从请求头获取 Creem 签名
  • 验证签名确保请求来自 Creem
  • 签名验证失败返回 401 未授权

事件解析和分发

const event = JSON.parse(body) as CreemWebhookEvent;

switch (event.eventType) {
  case "checkout.completed":
    await handleCheckoutCompleted(event);
    break;
  case "subscription.active":
    await handleSubscriptionActive(event);
    break;
  case "subscription.paid":
    await handleSubscriptionPaid(event);
    break;
  case "subscription.canceled":
    await handleSubscriptionCanceled(event);
    break;
  case "subscription.expired":
    await handleSubscriptionExpired(event);
    break;
  case "subscription.trialing":
    await handleSubscriptionTrialing(event);
    break;
  default:
    console.log(
      `Unhandled event type: ${event.eventType} ${JSON.stringify(event)}`
    );
}
  • 解析JSON事件数据
  • 使用 switch 语句分发不同类型的事件
  • 未处理的事件类型会记录日志

事件处理函数分析

结账完成处理

async function handleCheckoutCompleted(event: CreemWebhookEvent) {
  const checkout = event.object;
  console.log("Processing completed checkout:", checkout);

  try {
    // Create or update customer
    const customerId = await createOrUpdateCustomer(
      checkout.customer,
      checkout.metadata?.user_id
    );

    // Check if this is a credit purchase
    if (checkout.metadata?.product_type === "credits") {
      await addCreditsToCustomer(
        customerId,
        checkout.metadata?.credits,
        checkout.order.id,
        `Purchased ${checkout.metadata?.credits} credits`
      );
    }
    // If subscription exists, create or update it
    else if (checkout.subscription) {
      await createOrUpdateSubscription(checkout.subscription, customerId);
    }
  } catch (error) {
    console.error("Error handling checkout completed:", error);
    throw error;
  }
}

处理逻辑

  1. 客户管理: 创建或更新客户信息
  2. 产品类型判断: 区分积分购买和订阅购买
  3. 积分处理: 如果是积分购买,增加用户积分
  4. 订阅处理: 如果是订阅购买,创建或更新订阅

元数据使用

  • checkout.metadata?.user_id: 关联用户ID
  • checkout.metadata?.product_type: 产品类型
  • checkout.metadata?.credits: 积分数量

订阅激活处理

async function handleSubscriptionActive(event: CreemWebhookEvent) {
  const subscription = event.object;
  console.log("Processing active subscription:", subscription);

  try {
    // Create or update customer
    const customerId = await createOrUpdateCustomer(
      subscription.customer as any,
      subscription.metadata?.user_id
    );

    // Create or update subscription
    await createOrUpdateSubscription(subscription, customerId);
  } catch (error) {
    console.error("Error handling subscription active:", error);
    throw error;
  }
}

处理逻辑

  1. 客户信息同步: 确保客户信息是最新的
  2. 订阅状态更新: 将订阅状态更新为激活

订阅付费处理

async function handleSubscriptionPaid(event: CreemWebhookEvent) {
  const subscription = event.object;
  console.log("Processing paid subscription:", subscription);

  try {
    // Update subscription status and period
    const customerId = await createOrUpdateCustomer(
      subscription.customer as any,
      subscription.metadata?.user_id
    );
    await createOrUpdateSubscription(subscription, customerId);
  } catch (error) {
    console.error("Error handling subscription paid:", error);
    throw error;
  }
}

处理逻辑

  • 更新订阅状态和周期信息
  • 确保付费信息正确同步

订阅取消处理

async function handleSubscriptionCanceled(event: CreemWebhookEvent) {
  const subscription = event.object;
  console.log("Processing canceled subscription:", subscription);

  try {
    // Update subscription status
    const customerId = await createOrUpdateCustomer(
      subscription.customer as any,
      subscription.metadata?.user_id
    );
    await createOrUpdateSubscription(subscription, customerId);
  } catch (error) {
    console.error("Error handling subscription canceled:", error);
    throw error;
  }
}

处理逻辑

  • 更新订阅状态为取消
  • 保持客户信息同步

订阅过期处理

async function handleSubscriptionExpired(event: CreemWebhookEvent) {
  const subscription = event.object;
  console.log("Processing expired subscription:", subscription);

  try {
    // Update subscription status
    const customerId = await createOrUpdateCustomer(
      subscription.customer as any,
      subscription.metadata?.user_id
    );
    await createOrUpdateSubscription(subscription, customerId);
  } catch (error) {
    console.error("Error handling subscription expired:", error);
    throw error;
  }
}

处理逻辑

  • 更新订阅状态为过期
  • 触发相应的业务逻辑

订阅试用处理

async function handleSubscriptionTrialing(event: CreemWebhookEvent) {
  const subscription = event.object;
  console.log("Processing trialing subscription:", subscription);

  try {
    // Update subscription status
    const customerId = await createOrUpdateCustomer(
      subscription.customer as any,
      subscription.metadata?.user_id
    );
    await createOrUpdateSubscription(subscription, customerId);
  } catch (error) {
    console.error("Error handling subscription trialing:", error);
    throw error;
  }
}

处理逻辑

  • 更新订阅状态为试用
  • 设置试用期相关信息

设计模式分析

安全性设计

  1. 签名验证: 确保请求来自可信源
  2. 错误处理: 完善的异常处理机制
  3. 日志记录: 详细的操作日志

事件驱动架构

  1. 事件分发: 使用 switch 语句分发事件
  2. 处理函数分离: 每种事件类型有独立的处理函数
  3. 状态同步: 确保本地状态与 Creem 同步

幂等性设计

  1. 创建或更新: 使用 upsert 操作避免重复创建
  2. 状态检查: 处理重复事件的情况
  3. 事务完整性: 确保数据一致性

错误处理策略

全局错误处理

} catch (error) {
  console.error("Error processing webhook:", error);
  return new NextResponse("Webhook error", { status: 400 });
}

业务逻辑错误处理

} catch (error) {
  console.error("Error handling checkout completed:", error);
  throw error;
}

错误处理特点

  1. 日志记录: 详细记录错误信息
  2. 异常传播: 让调用者知道错误发生
  3. HTTP状态码: 返回合适的状态码

最佳实践总结

  1. 安全性: 签名验证确保请求安全
  2. 可靠性: 完善的错误处理机制
  3. 可维护性: 清晰的事件处理函数分离
  4. 监控性: 详细的日志记录
  5. 扩展性: 易于添加新的事件类型处理
  6. 幂等性: 处理重复事件的能力
  7. 性能: 异步处理提高响应速度

部署和监控考虑

环境配置

  • 确保 CREEM_WEBHOOK_SECRET 环境变量正确配置
  • 验证 Supabase 连接配置

监控指标

  • Webhook 接收成功率
  • 事件处理时间
  • 错误发生频率
  • 签名验证失败次数

调试建议

  1. 日志级别: 根据环境调整日志详细程度
  2. 事件重放: 支持事件重新处理
  3. 状态检查: 定期检查数据同步状态

这个 Webhook 处理路由是整个支付系统的核心组件,它确保了 Creem 支付系统与应用程序之间的可靠数据同步。通过完善的错误处理、安全验证和事件分发机制,它为整个支付流程提供了坚实的基础。

本文档为站内渲染。原始文件本地路径:saas/source/templates/模版-模版文档对比说明-08-API路由-creem-webhook-route-ts-677cbd.md(仅本地保留,不入库不部署)