# 如何使用 Dodo Payments 为 Nuxt.js 应用添加支付功能

> 分步介绍如何使用 Dodo Payments 为 Nuxt.js 应用集成支付、订阅和一次性购买功能。
- **Author**: Ayush Agarwal
- **Published**: 2026-04-06
- **Modified**: 2026-04-08
- **Category**: Integration, Nuxt.js, How-To
- **URL**: https://dodopayments.com/blogs/zh/accept-payments-nuxt-app

---

要在 Nuxt.js 应用中接受付款，请安装 Dodo Payments Node SDK，创建一个用于生成结账会话的 Nitro 服务器路由，再添加第二个用于验证传入 Webhook 的服务器路由。将这两部分都保留在服务器端，可以确保你的 API 密钥不会进入客户端构建包；同一模式同时适用于一次性购买和周期性订阅。

Nuxt.js 是目前最高效的 Vue 框架之一。它基于文件的路由、Nitro 服务器引擎以及一流的 TypeScript 支持，使其非常适合 SaaS 产品、数字市场和订阅工具。不过，为 Nuxt 应用添加可靠的支付层，仍然是大多数教程会跳过或浅尝辄止的部分。

本指南将从零开始介绍如何使用 [Dodo Payments](https://dodopayments.com) 完成 Nuxt payments 集成。你将直接使用 Dodo Payments Node SDK 为 Nuxt.js 应用添加支付功能，并全面掌控结账流程、Webhook 处理、订阅管理以及支付后的路由跳转。不需要魔法包装器，也没有黑盒。

如果你有 React 开发背景，我们还提供了 [add payments Next.js](https://dodopayments.com/blogs/add-payments-nextjs-app) 和 [add payments SvelteKit](https://dodopayments.com/blogs/sveltekit-payments-integration) 指南。如果你正在 [在 Astro 网站上接受付款](https://dodopayments.com/blogs/accept-payments-astro-sites)，同样的 Nitro 风格服务器路由模式也适用。

## 为什么选择 Nuxt.js 处理支付？

Nuxt.js 为支付集成提供了理想的架构。Nitro 服务器引擎允许你在 Nuxt 项目中定义服务器 API 路由，这意味着后端和前端可以位于同一个代码库中。你可以获得快速的初始页面加载、用于响应式状态管理的 composable，以及服务器专用代码与客户端安全代码之间清晰的分离。

将它与 [Dodo Payments](https://dodopayments.com) 搭配使用，你可以获得：

- MoR（名义商户）模式，由 Dodo 处理 VAT、GST 和全球税务合规
- 统一的 SDK，同时支持一次性付款和周期性订阅
- 支持签名验证的 Webhook 基础设施
- 支持 [overlay checkout](https://docs.dodopayments.com/developer-resources/overlay-checkout) 和 [inline checkout](https://docs.dodopayments.com/developer-resources/inline-checkout)，无需自定义重定向流程

对于正在构建 nuxt saas payments 方案，或需要快速拓展全球市场的 vue payments integration 的团队来说，这种组合从第一天起就能消除最棘手的合规和基础设施问题。查看 [Dodo Payments 定价](https://dodopayments.com/pricing)，了解相关费用。

> Nuxt 的 Nitro 服务器引擎是 Vue 生态中最简洁的服务器端支付逻辑方案之一。你的 API 密钥永远不会接触客户端构建包，而基于文件的服务器路由也能自然映射到支付和 Webhook 端点。这正是我们设计 Node SDK 时考虑的架构类型。
>
> - Ayush Agarwal，Dodo Payments 联合创始人兼 CPTO

## 集成流程

在编写任何代码之前，先了解使用 Dodo Payments 完成 nuxt.js payment gateway integration 的完整数据流：

```mermaid
flowchart TD
    subgraph Client ["Nuxt.js Frontend (Vue)"]
        A[Pricing Page] -->|User clicks Buy| B[CheckoutButton.vue]
        B -->|POST /api/checkout| C[Loading State]
        C -->|Redirect to checkout_url| D[Dodo Hosted Checkout]
        D -->|Payment Complete| E[Success Page]
        D -->|User Cancels| F[Cancel Page]
    end
    subgraph Server ["Nitro Server Routes"]
        G[POST /api/checkout] -->|Create Payment/Subscription| H[Dodo Payments API]
        H -->|Returns checkout_url| G
        I[POST /api/webhooks/dodo] -->|Verify Signature| J[Event Handlers]
        J -->|subscription.active| K[Update DB - Grant Access]
        J -->|payment.succeeded| L[Update DB - One-time]
        J -->|subscription.cancelled| M[Update DB - Revoke Access]
    end
    H -->|Async Webhook| I
```

客户端会向 Nitro 服务器路由发起 POST 请求，该路由使用 Dodo Payments Node SDK 创建结账会话。随后，用户会被重定向到托管结账页面。付款完成后，Dodo 会向你的服务器路由发送 Webhook，你可以在其中更新数据库。成功页和取消页负责处理支付后的用户体验。

## 第 1 步：设置项目

如果你还没有 Nuxt 3 项目，可以创建一个：

```bash
bunx nuxi@latest init my-nuxt-app
cd my-nuxt-app
```

安装 Dodo Payments Node SDK。这是 Node.js 后端通用的同一款 SDK。你可以在文档中找到它以及其他 [Dodo Payments SDK](https://docs.dodopayments.com/developer-resources/dodo-payments-sdks)。

```bash
npm install dodopayments
```

现在，你的 `package.json` 应该已将 `dodopayments` 列为依赖项。

## 第 2 步：环境变量

在项目根目录创建 `.env` 文件。切勿将此文件提交到版本控制系统。

```env
DODO_PAYMENTS_API_KEY=your_api_key_here
DODO_WEBHOOK_SECRET=your_webhook_secret_here
DODO_ENVIRONMENT=test_mode
DODO_RETURN_URL=http://localhost:3000/success
```

使用 `runtimeConfig` 在 `nuxt.config.ts` 中注册这些变量。`runtimeConfig` 下未放在 `public` 中的变量仅供服务器使用，永远不会暴露给客户端构建包。

```typescript
// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    dodoApiKey: process.env.DODO_PAYMENTS_API_KEY,
    dodoWebhookSecret: process.env.DODO_WEBHOOK_SECRET,
    dodoEnvironment: process.env.DODO_ENVIRONMENT || "test_mode",
    dodoReturnUrl: process.env.DODO_RETURN_URL,
  },
  compatibilityDate: "2024-11-01",
  devtools: { enabled: true },
});
```

在 Nitro 服务器路由中，你可以通过 `useRuntimeConfig()` 在服务器端访问这些值。

## 第 3 步：创建付款的服务器 API 路由

这是 add payments nuxt 模式中最重要的服务器端部分。你需要创建一个 Nitro 服务器路由，使用 Dodo Payments Node SDK 创建结账会话，并将托管结账 URL 返回给客户端。

创建文件 `server/api/checkout.post.ts`：

```typescript
// server/api/checkout.post.ts
import DodoPayments from "dodopayments";

export default defineEventHandler(async (event) => {
  const config = useRuntimeConfig();
  const body = await readBody(event);

  const { productId, quantity = 1, customerEmail } = body;

  if (!productId) {
    throw createError({
      statusCode: 400,
      statusMessage: "productId is required",
    });
  }

  const client = new DodoPayments({
    bearerToken: config.dodoApiKey,
    environment: config.dodoEnvironment as "test_mode" | "live_mode",
  });

  try {
    const payment = await client.payments.create({
      payment_link: true,
      billing: {
        city: "",
        country: "US",
        state: "",
        street: "",
        zipcode: "",
      },
      customer: {
        create_new_customer: true,
        email: customerEmail || "",
        name: "",
      },
      product_cart: [
        {
          product_id: productId,
          quantity,
        },
      ],
      return_url: config.dodoReturnUrl,
    });

    return {
      checkout_url: payment.payment_link,
      payment_id: payment.payment_id,
    };
  } catch (error: any) {
    throw createError({
      statusCode: 500,
      statusMessage: error?.message || "Failed to create checkout session",
    });
  }
});
```

对于订阅，将 `client.payments.create` 替换为 `client.subscriptions.create`，并传入映射到周期性方案的 `product_id`。返回结构相同：你会获得一个用于重定向用户的 URL。

```typescript
// server/api/subscribe.post.ts
import DodoPayments from "dodopayments";

export default defineEventHandler(async (event) => {
  const config = useRuntimeConfig();
  const body = await readBody(event);

  const { productId, customerEmail, userId } = body;

  const client = new DodoPayments({
    bearerToken: config.dodoApiKey,
    environment: config.dodoEnvironment as "test_mode" | "live_mode",
  });

  const subscription = await client.subscriptions.create({
    billing: {
      city: "",
      country: "US",
      state: "",
      street: "",
      zipcode: "",
    },
    customer: {
      create_new_customer: true,
      email: customerEmail,
      name: "",
    },
    product_id: productId,
    return_url: config.dodoReturnUrl,
    metadata: {
      userId,
    },
  });

  return {
    checkout_url: subscription.payment_link,
    subscription_id: subscription.subscription_id,
  };
});
```

传入 `metadata.userId` 至关重要。该值会出现在此订阅的每个 Webhook 事件中，让你无需额外查询，就能将 Dodo 事件与数据库中的用户关联起来。

## 第 4 步：前端结账按钮组件

现在创建 Vue 组件，用于调用服务器路由并将用户重定向到托管结账页面。该组件会将所有敏感逻辑保留在服务器端。

创建 `components/CheckoutButton.vue`：

```vue
<script setup lang="ts">
interface Props {
  productId: string;
  mode: "payment" | "subscription";
  label?: string;
}

const props = withDefaults(defineProps<Props>(), {
  label: "开始使用",
});

const isLoading = ref(false);
const errorMessage = ref("");

async function handleCheckout() {
  isLoading.value = true;
  errorMessage.value = "";

  const endpoint =
    props.mode === "subscription" ? "/api/subscribe" : "/api/checkout";

  try {
    const response = await $fetch<{ checkout_url: string }>(endpoint, {
      method: "POST",
      body: {
        productId: props.productId,
        quantity: 1,
      },
    });

    if (response.checkout_url) {
      window.location.href = response.checkout_url;
    }
  } catch (error: any) {
    errorMessage.value =
      error?.data?.statusMessage || "出了点问题，请重试。";
  } finally {
    isLoading.value = false;
  }
}
</script>

<template>
  <div>
    <button
      :disabled="isLoading"
      class="checkout-button"
      @click="handleCheckout"
    >
      <span v-if="isLoading">处理中...</span>
      <span v-else>{{ label }}</span>
    </button>
    <p v-if="errorMessage" class="error-text">{{ errorMessage }}</p>
  </div>
</template>

<style scoped>
.checkout-button {
  background-color: #4f46e5;
  color: white;
  padding: 0.75rem 1.5rem;
  border-radius: 0.5rem;
  font-weight: 600;
  border: none;
  cursor: pointer;
  transition: background-color 0.2s;
}

.checkout-button:hover:not(:disabled) {
  background-color: #4338ca;
}

.checkout-button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.error-text {
  color: #dc2626;
  font-size: 0.875rem;
  margin-top: 0.5rem;
}
</style>
```

在页面的任意位置使用该组件：

```vue
<!-- pages/pricing.vue -->
<script setup lang="ts">
// 你的定价页面逻辑
</script>

<template>
  <div class="pricing-page">
    <h1>选择方案</h1>

    <div class="plan-card">
      <h2>Pro 方案</h2>
      <p>$29/月</p>
      <CheckoutButton
        product-id="pdt_your_product_id_here"
        mode="subscription"
        label="开始使用 Pro 方案"
      />
    </div>

    <div class="plan-card">
      <h2>终身访问</h2>
      <p>$199，一次性付款</p>
      <CheckoutButton
        product-id="pdt_your_lifetime_id_here"
        mode="payment"
        label="购买终身访问权限"
      />
    </div>
  </div>
</template>
```

如果你希望用户留在当前页面，而不是被重定向，也可以集成 [overlay checkout](https://docs.dodopayments.com/developer-resources/overlay-checkout) 或 [inline checkout](https://docs.dodopayments.com/developer-resources/inline-checkout)。

## 第 5 步：使用 Nitro 服务器路由处理 Webhook

Webhook 是 Dodo Payments 告知后端支付结果的方式。它们会异步触发，并能处理用户在重定向完成前关闭浏览器的情况。这是可靠 nuxt saas payments 方案的基础，我们的 [支付通知 Webhook 指南](https://dodopayments.com/blogs/webhooks-payment-notifications)也介绍了通用模式。

完整的 [Webhook 文档](https://docs.dodopayments.com/developer-resources/webhooks)详细介绍了事件目录和签名验证。

创建 `server/api/webhooks/dodo.post.ts`：

```typescript
// server/api/webhooks/dodo.post.ts
import DodoPayments from "dodopayments";

export default defineEventHandler(async (event) => {
  const config = useRuntimeConfig();

  // 读取原始请求体以验证签名
  const rawBody = await readRawBody(event);
  const signature = getHeader(event, "webhook-signature") || "";
  const webhookId = getHeader(event, "webhook-id") || "";
  const webhookTimestamp = getHeader(event, "webhook-timestamp") || "";

  if (!rawBody) {
    throw createError({
      statusCode: 400,
      statusMessage: "请求体缺失",
    });
  }

  const client = new DodoPayments({
    bearerToken: config.dodoApiKey,
    environment: config.dodoEnvironment as "test_mode" | "live_mode",
    webhookKey: config.dodoWebhookSecret,
  });

  let webhookEvent: any;

  try {
    // 使用 SDK 验证 Webhook 签名
    webhookEvent = client.webhooks.unwrap(rawBody, {
      headers: {
        "webhook-id": webhookId,
        "webhook-signature": signature,
        "webhook-timestamp": webhookTimestamp,
      },
    });
  } catch (err) {
    throw createError({
      statusCode: 401,
      statusMessage: "Webhook 签名无效",
    });
  }

  const eventType = webhookEvent.type;
  const data = webhookEvent.data;

  try {
    switch (eventType) {
      case "payment.succeeded": {
        const { payment_id, customer, metadata } = data;
        // TODO: 更新你的数据库
        // 示例：await db.orders.create({ paymentId: payment_id, userId: metadata?.userId, status: 'completed' })
        console.log(`付款成功：${payment_id}，客户：${customer.email}`);
        break;
      }

      case "subscription.active": {
        const { subscription_id, customer, metadata } = data;
        const userId = metadata?.userId;
        // TODO: 在数据库中授予订阅访问权限
        // 示例：await db.users.update({ id: userId }, { subscriptionId: subscription_id, plan: 'pro' })
        console.log(`订阅已激活：${subscription_id}，客户：${customer.email}`);
        break;
      }

      case "subscription.renewed": {
        const { subscription_id, customer, next_billing_date } = data;
        // TODO: 更新数据库中的续费日期
        console.log(
          `订阅已续费：${subscription_id}，下次计费：${next_billing_date}`,
        );
        break;
      }

      case "subscription.cancelled": {
        const { subscription_id, metadata } = data;
        const userId = metadata?.userId;
        // TODO: 在数据库中撤销访问权限
        // 示例：await db.users.update({ id: userId }, { subscriptionId: null, plan: 'free' })
        console.log(`订阅已取消：${subscription_id}`);
        break;
      }

      case "payment.failed": {
        const { payment_id, customer } = data;
        console.log(`付款失败：${payment_id}，客户：${customer.email}`);
        break;
      }

      default:
        console.log(`未处理的事件类型：${eventType}`);
    }
  } catch (err) {
    console.error(`处理 Webhook 事件 ${eventType} 时出错：`, err);
    throw createError({
      statusCode: 500,
      statusMessage: "Webhook 处理失败",
    });
  }

  return { received: true };
});
```

关于此实现，需要注意以下几点：

- 始终先读取原始请求体，再进行任何其他处理。先将其解析为 JSON 会破坏签名验证。
- 让 Webhook 处理程序具备幂等性。如果你的服务器返回非 2xx 响应，Dodo 可能会重试 Webhook。将数据库操作封装在 upsert 逻辑中，或检查事件是否已经处理过。
- 在创建结账会话时传入的 `metadata.userId`，会出现在该付款或订阅的每个事件中。

> 在进行任何其他处理前读取原始请求体，对于 Webhook 签名验证至关重要。借助 readRawBody，Nitro 可以轻松完成这一点；但如果开发者先解析 JSON，再尝试验证签名，就会遇到空白字符和编码差异问题，导致验证悄悄失败。
>
> - Ayush Agarwal，Dodo Payments 联合创始人兼 CPTO

在 Dodo Payments 控制台的 Webhooks 部分注册你的 Webhook 端点 URL（`https://yourdomain.com/api/webhooks/dodo`）。

## 第 6 步：订阅管理

订阅在数据库中激活后，你需要让用户能够管理订阅。最常见的操作包括取消订阅和更改方案。查看[订阅管理](https://docs.dodopayments.com/features/subscription)文档，了解完整的事件目录。

在 `server/api/subscriptions/[id]/cancel.post.ts` 创建取消订阅的服务器路由：

```typescript
// server/api/subscriptions/[id]/cancel.post.ts
import DodoPayments from "dodopayments";

export default defineEventHandler(async (event) => {
  const config = useRuntimeConfig();
  const subscriptionId = getRouterParam(event, "id");

  if (!subscriptionId) {
    throw createError({
      statusCode: 400,
      statusMessage: "Subscription ID is required",
    });
  }

  const client = new DodoPayments({
    bearerToken: config.dodoApiKey,
    environment: config.dodoEnvironment as "test_mode" | "live_mode",
  });

  try {
    await client.subscriptions.update(subscriptionId, {
      status: "cancelled",
    });

    // TODO: 更新数据库以反映待处理的取消操作
    // 取消生效后，Dodo 会发送 subscription.cancelled Webhook

    return { cancelled: true };
  } catch (error: any) {
    throw createError({
      statusCode: 500,
      statusMessage: error?.message || "Failed to cancel subscription",
    });
  }
});
```

创建 composable `composables/useSubscription.ts`，在 Vue 组件中管理响应式订阅状态：

```typescript
// composables/useSubscription.ts
export interface SubscriptionState {
  plan: "free" | "pro" | "enterprise";
  subscriptionId: string | null;
  status: "active" | "cancelled" | "past_due" | null;
}

export const useSubscription = () => {
  const subscription = useState<SubscriptionState>("subscription", () => ({
    plan: "free",
    subscriptionId: null,
    status: null,
  }));

  const isPro = computed(
    () =>
      subscription.value.plan !== "free" &&
      subscription.value.status === "active",
  );

  async function cancelSubscription() {
    if (!subscription.value.subscriptionId) return;

    await $fetch(
      `/api/subscriptions/${subscription.value.subscriptionId}/cancel`,
      {
        method: "POST",
      },
    );

    subscription.value.status = "cancelled";
  }

  async function refreshSubscription(userId: string) {
    const data = await $fetch<SubscriptionState>(
      `/api/users/${userId}/subscription`,
    );
    subscription.value = data;
  }

  return {
    subscription,
    isPro,
    cancelSubscription,
    refreshSubscription,
  };
};
```

在任意 Vue 组件或页面中使用此 composable，为高级功能设置访问控制：

```vue
<!-- pages/dashboard.vue -->
<script setup lang="ts">
const { isPro, subscription } = useSubscription();
</script>

<template>
  <div>
    <h1>控制台</h1>

    <div v-if="isPro">
      <h2>Pro 功能</h2>
      <p>你可以使用所有高级功能。</p>
      <!-- 仅限 Pro 的内容 -->
    </div>

    <div v-else>
      <h2>升级到 Pro</h2>
      <p>解锁所有功能。</p>
      <CheckoutButton
        product-id="pdt_pro_plan_id"
        mode="subscription"
        label="升级到 Pro"
      />
    </div>
  </div>
</template>
```

如果你构建的是基于许可证访问的软件工具，而不是订阅产品，Dodo 还支持[许可证密钥](https://docs.dodopayments.com/features/license-keys)，你可以在用户完成一次性购买后将其分发给用户。

## 第 7 步：成功页和取消页

用户完成或放弃结账后，Dodo 会将其重定向到你配置的 `return_url`。你可以添加查询参数来区分成功和取消，也可以使用不同的 URL。

更新 `.env`：

```env
DODO_RETURN_URL=http://localhost:3000/checkout/result
```

或者使用不同的 URL：

```env
DODO_SUCCESS_URL=http://localhost:3000/success
DODO_CANCEL_URL=http://localhost:3000/cancel
```

创建 `pages/success.vue`：

```vue
<!-- pages/success.vue -->
<script setup lang="ts">
const route = useRoute();

// Dodo 会将 payment_id 或 subscription_id 作为查询参数附加到 URL
const paymentId = computed(() => (route.query.payment_id as string) || "");
const subscriptionId = computed(
  () => (route.query.subscription_id as string) || "",
);

useHead({
  title: "付款成功",
});
</script>

<template>
  <div class="result-page success">
    <div class="icon">✓</div>
    <h1>付款成功</h1>
    <p>感谢你的购买。你的账户已升级。</p>

    <div v-if="paymentId" class="detail">
      <span>付款 ID：</span> {{ paymentId }}
    </div>
    <div v-if="subscriptionId" class="detail">
      <span>订阅 ID：</span> {{ subscriptionId }}
    </div>

    <p class="note">
      你很快会收到确认邮件。如果没有看到，请检查垃圾邮件文件夹。
    </p>

    <NuxtLink to="/dashboard" class="cta-button"> 前往控制台 </NuxtLink>
  </div>
</template>
```

创建 `pages/cancel.vue`：

```vue
<!-- pages/cancel.vue -->
<script setup lang="ts">
useHead({
  title: "结账已取消",
});
</script>

<template>
  <div class="result-page cancel">
    <div class="icon">x</div>
    <h1>结账已取消</h1>
    <p>未产生任何费用。准备好后，你可以随时重试。</p>

    <NuxtLink to="/pricing" class="cta-button secondary">
      返回定价页面
    </NuxtLink>
  </div>
</template>
```

让成功页保持简洁。真正的状态更新通过 Webhook 完成，而不是通过重定向完成。即使用户在付款后关闭浏览器，也能获得访问权限，因为 `subscription.active` 或 `payment.succeeded` Webhook 会触发并更新数据库。

## 在本地测试集成

在本地测试结账流程需要完成几项准备。如果你想先查看更完整的检查清单，请参阅如何[在沙盒环境中测试支付集成](https://dodopayments.com/blogs/test-payment-integration-sandbox)。

- 设置 `DODO_ENVIRONMENT=test_mode`，并使用 Dodo 控制台中的测试 API 密钥
- 对于 Webhook：使用 ngrok 等隧道工具，将本地服务器暴露到互联网

启动 Nuxt 开发服务器：

```bash
npm run dev
```

在另一个终端中启动 ngrok：

```bash
ngrok http 3000
```

复制 HTTPS 转发 URL（例如 `https://abc123.ngrok-free.app`），并在 Dodo Payments 控制台中添加 Webhook 端点：`https://abc123.ngrok-free.app/api/webhooks/dodo`。

触发测试结账并使用测试卡完成付款后，你会在终端中看到 Webhook 触发，以及相应的处理程序运行。

如需完整的集成参考，包括所有可用事件，请参阅[集成指南](https://docs.dodopayments.com/developer-resources/integration-guide)。

## 最佳实践

- **在创建结账会话时传入 `metadata.userId`。** 没有它，你就无法在不进行额外数据库查询的情况下，将 Webhook 事件关联回特定用户。
- **让 Webhook 处理程序具备幂等性。** 使用 upsert 操作，并在应用变更前检查指定的 `payment_id` 或 `subscription_id` 是否已经处理过。
- **使用 Nitro 的仅服务器运行时配置。** 永远不要将 API 密钥放进 `runtimeConfig.public`。将其保存在顶层 `runtimeConfig` 对象中。
- **记录所有 Webhook 事件。** 即使是你不处理的事件也要记录。出现问题时，这能为你提供审计记录。
- **在结账组件中处理错误。** 网络故障难以避免。向用户显示有帮助的消息，并允许其重试，而无需完整刷新页面。
- **测试取消和续费 Webhook。** 大多数开发者会测试正常流程，却忘记测试订阅到期或续费时的情况。

对于正在构建 AI 产品的创始人，[适合 AI 初创公司的最佳支付平台](https://dodopayments.com/blogs/best-payment-platform-ai-startups)指南介绍了按使用量计费和预付费定价模式的具体计费注意事项。

## 常见问题

### `client.payments.create` 和 `client.subscriptions.create` 有什么区别？

`client.payments.create` 创建一次性收费。用户支付一次后，付款即完成。`client.subscriptions.create` 建立周期性计费关系，Dodo 会按照你定义的周期（每月、每年等）向客户收费，并在每个计费周期发送 `subscription.renewed` Webhook。终身优惠或一次性购买使用 payments，SaaS 方案则使用 subscriptions。

### 我需要 `@dodopayments/nuxt` 模块，还是只需要 Node SDK？

两者都可以。`@dodopayments/nuxt` 模块是一个便利包装器，会自动注册 composable，并简化服务器路由设置。直接使用 `dodopayments` Node SDK 可以获得更多控制权，也更容易沿用你在其他 Node.js 后端中使用的相同模式。本指南直接使用 SDK，让代码更明确、更易移植。

### 如何根据订阅状态保护 Nuxt 中的路由？

在每次相关 Webhook 事件发生后，将用户的订阅状态存储到数据库中。创建一个 Nitro 服务器路由，返回指定用户当前的订阅状态。在 Nuxt 页面中，使用调用此端点的 `definePageMeta` 路由中间件，并将未认证或未订阅的用户重定向。你也可以在任意页面顶部通过 `useAsyncData` 检查订阅状态，并使用 `navigateTo` 进行重定向。

### 我可以在 Cloudflare Workers 上将 Dodo Payments 与 Nuxt 搭配使用吗？

可以。Nuxt 3 通过 Nitro preset 支持 Cloudflare Workers。`dodopayments` SDK 是标准 Node.js 软件包，只要边缘运行时提供 `fetch` API 就可以运行；Cloudflare Workers 提供了该 API。将 `DODO_PAYMENTS_API_KEY` 和 `DODO_WEBHOOK_SECRET` 设置为 Cloudflare Worker secrets，然后像往常一样通过 `useRuntimeConfig()` 访问它们。

### 如果 Webhook 处理失败会怎样？

Dodo Payments 会通过指数退避重试失败的 Webhook。如果你的服务器返回非 2xx 状态码，Dodo 会在接下来的几个小时内多次重试请求。因此幂等性非常重要：如果同一个 `subscription.active` 事件被传送两次，你的数据库更新在两次处理后都应产生相同结果。使用以 `subscription_id` 或 `payment_id` 为键的 upsert 操作，可以安全地处理重试。

## 总结

使用 Dodo Payments 为 Nuxt.js 应用添加支付功能，主要包含四个部分：使用 Node SDK 创建结账会话的服务器 API 路由；调用该路由并重定向用户的 Vue 组件；验证签名并更新数据库的 Nitro Webhook 处理程序；以及处理结账后用户体验的成功页和取消页。

本指南介绍的 nuxt.js payment gateway integration 模式，既适用于简单的一次性购买，也适用于完整的订阅管理系统。由于 Dodo 作为你的 MoR（名义商户），你无需处理全球税务合规、VAT 注册和拒付管理的复杂性。

如需完整的 SDK 参考，请查看 [Dodo Payments SDK](https://docs.dodopayments.com/developer-resources/dodo-payments-sdks)页面；如需完整的 API 操作流程，请查看[集成指南](https://docs.dodopayments.com/developer-resources/integration-guide)。准备上线时，查看 [Dodo Payments 定价](https://dodopayments.com/pricing)，并将环境变量从 `test_mode` 切换为 `live_mode`。
---
- [More Integration articles](https://dodopayments.com/blogs/category/integration)
- [All articles](https://dodopayments.com/blogs)