mercadopago-integration

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

MercadoPago Checkout Pro - Next.js Integration

MercadoPago Checkout Pro - Next.js 集成

Redirect-based payment flow: buyer clicks "Pay", is redirected to MercadoPago, completes payment, returns to the app. A webhook confirms the payment status in the background.
Pay button --> POST /api/checkout (create preference) --> Redirect to MercadoPago
                                                               |
MercadoPago --webhook--> /api/webhooks/mercadopago --> Update DB
          |
          +--redirect--> /payment-success?purchase=ID --> Verify via API
基于重定向的支付流程:买家点击“支付”按钮,被重定向到MercadoPago完成支付,之后返回应用。Webhook会在后台确认支付状态。
Pay button --> POST /api/checkout (create preference) --> Redirect to MercadoPago
                                                               |
MercadoPago --webhook--> /api/webhooks/mercadopago --> Update DB
          |
          +--redirect--> /payment-success?purchase=ID --> Verify via API

Before Starting

开始之前

  1. Determine the database adapter. Explore the codebase or ask the user:
    • Supabase? See
      references/database-supabase.md
      for DB helper implementation
    • Prisma? See
      references/database-prisma.md
      for DB helper implementation
    • Raw PostgreSQL (pg, Drizzle, or other)? See
      references/database-postgresql.md
      for DB helper implementation
  2. Gather or infer from the codebase:
DetailWhyExample
CurrencyPreference creation
ARS
,
BRL
,
MXN
(see
references/countries.md
)
Success/failure routes
back_urls
in preference
/payment-success
,
/pago-exitoso
Brand nameCard statement descriptor
MY_STORE
Product/item tableFK in
purchase_items
products
,
photos
,
courses
Cart store locationHook reads items from it
src/store/cart.ts
DB client pathAPI routes import it
src/lib/supabase/server.ts
,
src/lib/prisma.ts
  1. 确定数据库适配器。查看代码库或询问用户:
    • Supabase? 查看
      references/database-supabase.md
      获取数据库助手实现方案
    • Prisma? 查看
      references/database-prisma.md
      获取数据库助手实现方案
    • 原生PostgreSQL(pg、Drizzle或其他)? 查看
      references/database-postgresql.md
      获取数据库助手实现方案
  2. 从代码库中收集或推断以下信息:
详情原因示例
币种创建支付偏好时需要
ARS
,
BRL
,
MXN
(参考
references/countries.md
成功/失败路由支付偏好中的
back_urls
配置
/payment-success
,
/pago-exitoso
品牌名称信用卡账单描述符
MY_STORE
产品/商品表
purchase_items
中的外键关联
products
,
photos
,
courses
购物车存储位置Hook从该位置读取商品
src/store/cart.ts
数据库客户端路径API路由会导入该文件
src/lib/supabase/server.ts
,
src/lib/prisma.ts

Prerequisites

前置条件

  1. Install dependencies:
    npm install mercadopago zod
  2. Set environment variables (never prefix access token with
    NEXT_PUBLIC_
    ):
    env
    MERCADOPAGO_ACCESS_TOKEN=TEST-xxxx   # from https://www.mercadopago.com/developers/panel/app
    NEXT_PUBLIC_APP_URL=http://localhost:3000  # HTTPS in production
  3. Run database migration from
    assets/migration.sql
    (works on any PostgreSQL database).
  1. 安装依赖:
    npm install mercadopago zod
  2. 设置环境变量(绝对不要为访问令牌添加
    NEXT_PUBLIC_
    前缀):
    env
    MERCADOPAGO_ACCESS_TOKEN=TEST-xxxx   # 来自 https://www.mercadopago.com/developers/panel/app
    NEXT_PUBLIC_APP_URL=http://localhost:3000  # 生产环境使用HTTPS
  3. 运行
    assets/migration.sql
    中的数据库迁移脚本(适用于任意PostgreSQL数据库)。

Implementation Steps

实现步骤

Step 1: Database Helper

步骤1:数据库助手

Create:
src/lib/db/purchases.ts
This abstracts all purchase DB operations. Implement using your DB adapter. See the reference file for your adapter:
  • Supabase:
    references/database-supabase.md
  • Prisma:
    references/database-prisma.md
  • Raw pg / other:
    references/database-postgresql.md
The helper must export these functions:
typescript
interface PurchaseInsert {
  user_email: string;
  status: 'pending';
  total_amount: number;
}

interface PurchaseUpdate {
  status?: 'pending' | 'approved' | 'rejected';
  mercadopago_payment_id?: string;
  mercadopago_preference_id?: string;
  user_email?: string;
  updated_at?: string;
}

// Required exports:
export async function createPurchase(data: PurchaseInsert): Promise<{ id: string }>;
export async function updatePurchase(id: string, data: PurchaseUpdate): Promise<void>;
export async function getPurchaseStatus(id: string): Promise<{ id: string; status: string } | null>;
export async function createPurchaseItems(purchaseId: string, items: { item_id: string; price: number }[]): Promise<void>;
创建文件:
src/lib/db/purchases.ts
该文件封装所有订单相关的数据库操作。根据你的数据库适配器实现对应逻辑。查看对应适配器的参考文件:
  • Supabase:
    references/database-supabase.md
  • Prisma:
    references/database-prisma.md
  • 原生pg / 其他:
    references/database-postgresql.md
该助手必须导出以下函数:
typescript
interface PurchaseInsert {
  user_email: string;
  status: 'pending';
  total_amount: number;
}

interface PurchaseUpdate {
  status?: 'pending' | 'approved' | 'rejected';
  mercadopago_payment_id?: string;
  mercadopago_preference_id?: string;
  user_email?: string;
  updated_at?: string;
}

// 必须导出的函数:
export async function createPurchase(data: PurchaseInsert): Promise<{ id: string }>;
export async function updatePurchase(id: string, data: PurchaseUpdate): Promise<void>;
export async function getPurchaseStatus(id: string): Promise<{ id: string; status: string } | null>;
export async function createPurchaseItems(purchaseId: string, items: { item_id: string; price: number }[]): Promise<void>;

Step 2: MercadoPago Client

步骤2:MercadoPago客户端

Create:
src/lib/mercadopago/client.ts
typescript
import { MercadoPagoConfig, Preference, Payment } from 'mercadopago';

const client = new MercadoPagoConfig({
  accessToken: process.env.MERCADOPAGO_ACCESS_TOKEN!,
});

const preference = new Preference(client);
const payment = new Payment(client);

interface CreatePreferenceParams {
  items: { id: string; title: string; quantity: number; unit_price: number }[];
  purchaseId: string;
  buyerEmail?: string;
}

export async function createPreference({
  items, purchaseId, buyerEmail,
}: CreatePreferenceParams) {
  const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';

  return preference.create({
    body: {
      items: items.map((item) => ({
        id: item.id,
        title: item.title,
        quantity: item.quantity,
        unit_price: item.unit_price,
        currency_id: 'ARS', // Change per references/countries.md
      })),
      ...(buyerEmail ? { payer: { email: buyerEmail } } : {}),
      back_urls: {
        success: `${baseUrl}/payment-success?purchase=${purchaseId}`,
        failure: `${baseUrl}/payment-failure?purchase=${purchaseId}`,
        pending: `${baseUrl}/payment-success?purchase=${purchaseId}&status=pending`,
      },
      // CRITICAL: auto_return requires HTTPS. Omit on localhost or MP returns 400.
      ...(baseUrl.startsWith('https') ? { auto_return: 'approved' as const } : {}),
      external_reference: purchaseId,
      notification_url: `${baseUrl}/api/webhooks/mercadopago`,
      statement_descriptor: 'YOUR_BRAND', // Replace with user's brand
      expires: true,
      expiration_date_from: new Date().toISOString(),
      expiration_date_to: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
    },
  });
}

export async function getPayment(paymentId: string) {
  return payment.get({ id: paymentId });
}
创建文件:
src/lib/mercadopago/client.ts
typescript
import { MercadoPagoConfig, Preference, Payment } from 'mercadopago';

const client = new MercadoPagoConfig({
  accessToken: process.env.MERCADOPAGO_ACCESS_TOKEN!,
});

const preference = new Preference(client);
const payment = new Payment(client);

interface CreatePreferenceParams {
  items: { id: string; title: string; quantity: number; unit_price: number }[];
  purchaseId: string;
  buyerEmail?: string;
}

export async function createPreference({
  items, purchaseId, buyerEmail,
}: CreatePreferenceParams) {
  const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';

  return preference.create({
    body: {
      items: items.map((item) => ({
        id: item.id,
        title: item.title,
        quantity: item.quantity,
        unit_price: item.unit_price,
        currency_id: 'ARS', // 根据 references/countries.md 修改
      })),
      ...(buyerEmail ? { payer: { email: buyerEmail } } : {}),
      back_urls: {
        success: `${baseUrl}/payment-success?purchase=${purchaseId}`,
        failure: `${baseUrl}/payment-failure?purchase=${purchaseId}`,
        pending: `${baseUrl}/payment-success?purchase=${purchaseId}&status=pending`,
      },
      // 关键:auto_return 需要HTTPS。在本地环境请省略,否则MercadoPago会返回400错误。
      ...(baseUrl.startsWith('https') ? { auto_return: 'approved' as const } : {}),
      external_reference: purchaseId,
      notification_url: `${baseUrl}/api/webhooks/mercadopago`,
      statement_descriptor: 'YOUR_BRAND', // 替换为用户的品牌名称
      expires: true,
      expiration_date_from: new Date().toISOString(),
      expiration_date_to: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
    },
  });
}

export async function getPayment(paymentId: string) {
  return payment.get({ id: paymentId });
}

Step 3: Checkout API Route

步骤3:结账API路由

Create:
src/app/api/checkout/route.ts
typescript
import { NextResponse } from 'next/server';
import { createPurchase, updatePurchase } from '@/lib/db/purchases';
import { createPreference } from '@/lib/mercadopago/client';
import { z } from 'zod';

const checkoutSchema = z.object({
  items: z.array(z.object({
    id: z.string(),
    title: z.string(),
    quantity: z.number().positive(),
    unit_price: z.number().positive(),
  })).min(1),
  email: z.string().email().optional(),
});

export async function POST(request: Request) {
  try {
    const body = await request.json();
    const validation = checkoutSchema.safeParse(body);
    if (!validation.success) {
      return NextResponse.json({ error: 'Invalid request data' }, { status: 400 });
    }

    const { items, email } = validation.data;
    const totalAmount = items.reduce((sum, i) => sum + i.unit_price * i.quantity, 0);

    const purchase = await createPurchase({
      user_email: email || 'pending@checkout',
      status: 'pending',
      total_amount: totalAmount,
    });

    const mpPreference = await createPreference({
      items, purchaseId: purchase.id, buyerEmail: email,
    });

    await updatePurchase(purchase.id, {
      mercadopago_preference_id: mpPreference.id,
    });

    return NextResponse.json({
      preferenceId: mpPreference.id,
      initPoint: mpPreference.init_point,
      purchaseId: purchase.id,
    });
  } catch (error) {
    console.error('Checkout error:', error);
    return NextResponse.json({ error: 'Checkout failed' }, { status: 500 });
  }
}
创建文件:
src/app/api/checkout/route.ts
typescript
import { NextResponse } from 'next/server';
import { createPurchase, updatePurchase } from '@/lib/db/purchases';
import { createPreference } from '@/lib/mercadopago/client';
import { z } from 'zod';

const checkoutSchema = z.object({
  items: z.array(z.object({
    id: z.string(),
    title: z.string(),
    quantity: z.number().positive(),
    unit_price: z.number().positive(),
  })).min(1),
  email: z.string().email().optional(),
});

export async function POST(request: Request) {
  try {
    const body = await request.json();
    const validation = checkoutSchema.safeParse(body);
    if (!validation.success) {
      return NextResponse.json({ error: 'Invalid request data' }, { status: 400 });
    }

    const { items, email } = validation.data;
    const totalAmount = items.reduce((sum, i) => sum + i.unit_price * i.quantity, 0);

    const purchase = await createPurchase({
      user_email: email || 'pending@checkout',
      status: 'pending',
      total_amount: totalAmount,
    });

    const mpPreference = await createPreference({
      items, purchaseId: purchase.id, buyerEmail: email,
    });

    await updatePurchase(purchase.id, {
      mercadopago_preference_id: mpPreference.id,
    });

    return NextResponse.json({
      preferenceId: mpPreference.id,
      initPoint: mpPreference.init_point,
      purchaseId: purchase.id,
    });
  } catch (error) {
    console.error('Checkout error:', error);
    return NextResponse.json({ error: 'Checkout failed' }, { status: 500 });
  }
}

Step 4: Webhook Handler

步骤4:Webhook处理器

Create:
src/app/api/webhooks/mercadopago/route.ts
typescript
import { NextResponse } from 'next/server';
import { getPurchaseStatus, updatePurchase } from '@/lib/db/purchases';
import { getPayment } from '@/lib/mercadopago/client';

export async function POST(request: Request) {
  try {
    const body = await request.json();
    if (body.type !== 'payment' && body.action !== 'payment.created' && body.action !== 'payment.updated') {
      return NextResponse.json({ received: true });
    }

    const paymentId = body.data?.id;
    if (!paymentId) return NextResponse.json({ received: true });

    const payment = await getPayment(paymentId.toString());
    if (!payment?.external_reference) return NextResponse.json({ received: true });

    let status: 'pending' | 'approved' | 'rejected' = 'pending';
    if (payment.status === 'approved') status = 'approved';
    else if (['rejected', 'cancelled', 'refunded'].includes(payment.status || '')) status = 'rejected';

    // Idempotency: skip if already in terminal state
    const existing = await getPurchaseStatus(payment.external_reference);
    if (existing?.status === 'approved' || existing?.status === 'rejected') {
      return NextResponse.json({ received: true });
    }

    const payerEmail = payment.payer?.email;
    await updatePurchase(payment.external_reference, {
      status,
      mercadopago_payment_id: paymentId.toString(),
      ...(payerEmail ? { user_email: payerEmail } : {}),
      updated_at: new Date().toISOString(),
    });

    return NextResponse.json({ received: true });
  } catch (error) {
    console.error('Webhook error:', error);
    return NextResponse.json({ error: 'Webhook processing failed' }, { status: 500 });
  }
}

// GET endpoint for MercadoPago verification pings
export async function GET() {
  return NextResponse.json({ status: 'ok' });
}
创建文件:
src/app/api/webhooks/mercadopago/route.ts
typescript
import { NextResponse } from 'next/server';
import { getPurchaseStatus, updatePurchase } from '@/lib/db/purchases';
import { getPayment } from '@/lib/mercadopago/client';

export async function POST(request: Request) {
  try {
    const body = await request.json();
    if (body.type !== 'payment' && body.action !== 'payment.created' && body.action !== 'payment.updated') {
      return NextResponse.json({ received: true });
    }

    const paymentId = body.data?.id;
    if (!paymentId) return NextResponse.json({ received: true });

    const payment = await getPayment(paymentId.toString());
    if (!payment?.external_reference) return NextResponse.json({ received: true });

    let status: 'pending' | 'approved' | 'rejected' = 'pending';
    if (payment.status === 'approved') status = 'approved';
    else if (['rejected', 'cancelled', 'refunded'].includes(payment.status || '')) status = 'rejected';

    // 幂等性处理:如果订单已处于最终状态则跳过更新
    const existing = await getPurchaseStatus(payment.external_reference);
    if (existing?.status === 'approved' || existing?.status === 'rejected') {
      return NextResponse.json({ received: true });
    }

    const payerEmail = payment.payer?.email;
    await updatePurchase(payment.external_reference, {
      status,
      mercadopago_payment_id: paymentId.toString(),
      ...(payerEmail ? { user_email: payerEmail } : {}),
      updated_at: new Date().toISOString(),
    });

    return NextResponse.json({ received: true });
  } catch (error) {
    console.error('Webhook error:', error);
    return NextResponse.json({ error: 'Webhook processing failed' }, { status: 500 });
  }
}

// GET端点用于MercadoPago的验证请求
export async function GET() {
  return NextResponse.json({ status: 'ok' });
}

Step 5: Purchase Status API

步骤5:订单状态API

Create:
src/app/api/purchases/[id]/route.ts
typescript
import { NextResponse } from 'next/server';
import { getPurchaseStatus } from '@/lib/db/purchases';

export async function GET(
  _request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const data = await getPurchaseStatus(id);

  if (!data) return NextResponse.json({ error: 'Not found' }, { status: 404 });
  return NextResponse.json({ id: data.id, status: data.status });
}
创建文件:
src/app/api/purchases/[id]/route.ts
typescript
import { NextResponse } from 'next/server';
import { getPurchaseStatus } from '@/lib/db/purchases';

export async function GET(
  _request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const data = await getPurchaseStatus(id);

  if (!data) return NextResponse.json({ error: 'Not found' }, { status: 404 });
  return NextResponse.json({ id: data.id, status: data.status });
}

Step 6: Checkout Hook (Frontend)

步骤6:结账Hook(前端)

Create:
src/hooks/useCheckout.ts
Double-click prevention uses
useRef
(survives re-renders, unlike
useState
).
typescript
'use client';
import { useCallback, useRef, useState } from 'react';

export function useCheckout() {
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const guard = useRef(false);

  const submitCheckout = useCallback(async (items: unknown[]) => {
    if (guard.current) return;
    setError(null);
    guard.current = true;
    setIsSubmitting(true);
    try {
      const res = await fetch('/api/checkout', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ items }),
      });
      const data = await res.json();
      if (!res.ok) throw new Error(data.error || 'Checkout failed');
      if (data.initPoint) window.location.href = data.initPoint;
      else throw new Error('No payment link returned');
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error');
      setIsSubmitting(false);
      guard.current = false;
    }
  }, []);

  return { submitCheckout, isSubmitting, error };
}
创建文件:
src/hooks/useCheckout.ts
使用
useRef
防止重复提交(它会在重渲染后保留状态,不像
useState
)。
typescript
'use client';
import { useCallback, useRef, useState } from 'react';

export function useCheckout() {
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const guard = useRef(false);

  const submitCheckout = useCallback(async (items: unknown[]) => {
    if (guard.current) return;
    setError(null);
    guard.current = true;
    setIsSubmitting(true);
    try {
      const res = await fetch('/api/checkout', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ items }),
      });
      const data = await res.json();
      if (!res.ok) throw new Error(data.error || 'Checkout failed');
      if (data.initPoint) window.location.href = data.initPoint;
      else throw new Error('No payment link returned');
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error');
      setIsSubmitting(false);
      guard.current = false;
    }
  }, []);

  return { submitCheckout, isSubmitting, error };
}

Step 7: Success Page with Verification

步骤7:带验证的成功页面

Create:
src/app/payment-success/page.tsx
(adjust route name)
Always verify purchase status server-side. Never trust the redirect URL alone. Wrap
useSearchParams
in
<Suspense>
(Next.js App Router requirement).
tsx
'use client';
import { useSearchParams } from 'next/navigation';
import { Suspense, useCallback, useEffect, useState } from 'react';

type Status = 'loading' | 'approved' | 'pending' | 'rejected' | 'error';

function PaymentResult() {
  const purchaseId = useSearchParams().get('purchase');
  const [status, setStatus] = useState<Status>(purchaseId ? 'loading' : 'approved');

  const verify = useCallback(async (id: string) => {
    try {
      const res = await fetch(`/api/purchases/${id}`);
      if (!res.ok) { setStatus('error'); return; }
      const { status } = await res.json();
      setStatus(status === 'approved' ? 'approved'
        : status === 'pending' ? 'pending' : 'rejected');
    } catch { setStatus('error'); }
  }, []);

  useEffect(() => { if (purchaseId) verify(purchaseId); }, [purchaseId, verify]);

  // Render UI based on status: loading/approved/pending/rejected/error
  return <div>{/* Customize UI per status */}</div>;
}

export default function PaymentSuccessPage() {
  return <Suspense fallback={<div>Loading...</div>}><PaymentResult /></Suspense>;
}
创建文件:
src/app/payment-success/page.tsx
(根据需要调整路由名称)
始终通过服务端验证订单状态。绝对不要仅信任重定向URL。将
useSearchParams
包裹在
<Suspense>
中(Next.js App Router 的要求)。
tsx
'use client';
import { useSearchParams } from 'next/navigation';
import { Suspense, useCallback, useEffect, useState } from 'react';

type Status = 'loading' | 'approved' | 'pending' | 'rejected' | 'error';

function PaymentResult() {
  const purchaseId = useSearchParams().get('purchase');
  const [status, setStatus] = useState<Status>(purchaseId ? 'loading' : 'approved');

  const verify = useCallback(async (id: string) => {
    try {
      const res = await fetch(`/api/purchases/${id}`);
      if (!res.ok) { setStatus('error'); return; }
      const { status } = await res.json();
      setStatus(status === 'approved' ? 'approved'
        : status === 'pending' ? 'pending' : 'rejected');
    } catch { setStatus('error'); }
  }, []);

  useEffect(() => { if (purchaseId) verify(purchaseId); }, [purchaseId, verify]);

  // 根据状态渲染UI:loading/approved/pending/rejected/error
  return <div>{/* 根据状态自定义UI */}</div>;
}

export default function PaymentSuccessPage() {
  return <Suspense fallback={<div>加载中...</div>}><PaymentResult /></Suspense>;
}

Critical Gotchas

关键注意事项

For detailed solutions, see
references/troubleshooting.md
.
GotchaFix
auto_return
+ localhost = 400 error
Only set when URL starts with
https
user_email NOT NULL
+ no email = 500
Use
'pending@checkout'
placeholder; webhook updates it
Hydration mismatch (localStorage cart)Add
mounted
state guard before rendering cart content
Double purchase on double-clickUse
useRef
guard, not just
useState
Success page trusts redirect URLAlways verify via
/api/purchases/[id]
Webhook duplicate updatesCheck if purchase is already terminal before updating
Webhooks can't reach localhostUse ngrok:
ngrok http 3000
useSearchParams
error
Wrap component in
<Suspense>
如需详细解决方案,请查看
references/troubleshooting.md
问题修复方案
auto_return
+ 本地环境 = 400错误
仅当URL以
https
开头时才设置该参数
user_email NOT NULL
+ 无邮箱 = 500错误
使用
'pending@checkout'
作为占位符;Webhook会更新该值
Hydration不匹配(本地存储购物车)在渲染购物车内容前添加
mounted
状态判断
双击导致重复下单使用
useRef
进行拦截,不要仅依赖
useState
成功页面信任重定向URL始终通过
/api/purchases/[id]
验证状态
Webhook重复更新更新前检查订单是否已处于最终状态
Webhook无法访问本地环境使用ngrok:
ngrok http 3000
useSearchParams
报错
将组件包裹在
<Suspense>

Checklist

检查清单

  • mercadopago
    +
    zod
    installed
  • MERCADOPAGO_ACCESS_TOKEN
    in
    .env
    (TEST token for dev, never
    NEXT_PUBLIC_
    )
  • NEXT_PUBLIC_APP_URL
    in
    .env
    (HTTPS in production)
  • Database migration run (
    purchases
    +
    purchase_items
    )
  • DB helper implemented (
    src/lib/db/purchases.ts
    )
  • /api/checkout
    with Zod validation
  • /api/webhooks/mercadopago
    with idempotency check
  • /api/purchases/[id]
    for status verification
  • Success page verifies status (not trusting redirect)
  • auto_return
    only for HTTPS
  • Checkout hook prevents double submit
  • useSearchParams
    wrapped in
    <Suspense>
  • 已安装
    mercadopago
    +
    zod
  • .env
    中已配置
    MERCADOPAGO_ACCESS_TOKEN
    (开发环境使用测试令牌,绝对不要添加
    NEXT_PUBLIC_
    前缀)
  • .env
    中已配置
    NEXT_PUBLIC_APP_URL
    (生产环境使用HTTPS)
  • 已运行数据库迁移脚本(创建
    purchases
    purchase_items
    表)
  • 已实现数据库助手(
    src/lib/db/purchases.ts
  • /api/checkout
    已添加Zod验证
  • /api/webhooks/mercadopago
    已添加幂等性检查
  • /api/purchases/[id]
    已实现状态验证
  • 成功页面会验证状态(不依赖重定向URL)
  • auto_return
    仅在HTTPS环境下启用
  • 结账Hook已防止重复提交
  • useSearchParams
    已包裹在
    <Suspense>

References

参考资料

  • references/database-supabase.md
    - Supabase DB helper implementation
  • references/database-prisma.md
    - Prisma DB helper implementation
  • references/database-postgresql.md
    - Raw PostgreSQL (pg, Drizzle, etc.) DB helper implementation
  • references/troubleshooting.md
    - Detailed error fixes and solutions
  • references/countries.md
    - Currencies, available countries, and MercadoPago specifics
  • references/usage-examples.md
    - Ready-to-use prompt templates
  • assets/migration.sql
    - Database schema template (standard PostgreSQL)
  • MercadoPago Checkout Pro Docs
  • MercadoPago Node SDK
  • references/database-supabase.md
    - Supabase数据库助手实现方案
  • references/database-prisma.md
    - Prisma数据库助手实现方案
  • references/database-postgresql.md
    - 原生PostgreSQL(pg、Drizzle等)数据库助手实现方案
  • references/troubleshooting.md
    - 详细的错误修复方案
  • references/countries.md
    - 币种、支持国家及MercadoPago相关细节
  • references/usage-examples.md
    - 可直接使用的提示模板
  • assets/migration.sql
    - 数据库 schema 模板(标准PostgreSQL)
  • MercadoPago Checkout Pro 文档
  • MercadoPago Node SDK