mcpField
S
service-integration

service-integration

github.com/mgd34msu开发653 次安装10 周前更新
代码仓库

关于 service-integration

service-integration 是将外部服务接入应用的技能,涵盖服务选择和验证。内容涉及常见服务类型,并说明错误处理、重试及不调用生产 API 的集成测试方法。

主要功能

  1. 01服务选择指导
  2. 02邮件、CMS、上传和分析集成模式
  3. 03Webhook 配置与验证
  4. 04重试和熔断器模式
  5. 05基于模拟的集成测试
  6. 06验证脚本

适用场景

  1. 01接入邮件服务商或 CMS
  2. 02集成文件上传或分析服务
  3. 03处理外部调用的重试与故障
  4. 04无需生产 API 测试 Webhook

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/mgd34msu/goodvibes-plugin --skill service-integration

安装后在 Claude Code 中输入/service-integration确认已加载。

附带文件references/service-patterns.mdscripts/validate-services.sh

详细介绍

资源

scripts/
  validate-services.sh
references/
  service-patterns.md

服务集成实施

本技能将指导你完成将外部服务集成到应用程序的全过程,从服务选择到生产环境部署。它利用 GoodVibes 精准工具,实现类型安全、弹性强的服务集成,并提供适当的错误处理和测试。

何时使用此技能

在以下情况下使用此技能:

  • 集成电子邮件服务提供商(Resend、SendGrid、Postmark)
  • 设置内容管理系统(Sanity、Contentful、Payload、Strapi)
  • 实现文件上传服务(UploadThing、Cloudinary、S3)
  • 添加分析和跟踪(PostHog、Plausible、Google Analytics)
  • 为第三方服务配置 webhook 端点
  • 实现重试逻辑和断路器
  • 在不调用生产 API 的情况下测试服务集成

前提条件

所需背景信息:

  • 服务需求(电子邮件、CMS、上传、分析)
  • 预期流量和规模要求
  • 预算限制
  • 可自托管服务与托管服务的偏好

工具:

  • precision_grep,用于检测现有集成
  • precision_read,用于分析服务配置
  • precision_write,用于创建集成代码
  • precision_exec,用于测试和验证
  • discover,用于批量分析

阶段 1:发现——检测现有集成

添加新服务之前,先分析现有集成,以保持一致性。

步骤 1.1:检测服务 SDK 和模式

使用 discover 并行分析多个方面:

discover:
  queries:
    - id: email-sdks
      type: grep
      pattern: "(resend|sendgrid|postmark|nodemailer)"
      glob: "package.json"
    
    - id: cms-sdks
      type: grep
      pattern: "(@sanity|contentful|@payloadcms|@strapi)"
      glob: "package.json"
    
    - id: upload-sdks
      type: grep
      pattern: "(uploadthing|cloudinary|@aws-sdk/client-s3)"
      glob: "package.json"
    
    - id: analytics-sdks
      type: grep
      pattern: "(posthog-js|plausible|@vercel/analytics)"
      glob: "package.json"
    
    - id: api-keys-env
      type: grep
      pattern: "(RESEND_|SENDGRID_|SANITY_|CONTENTFUL_|UPLOADTHING_|CLOUDINARY_|AWS_|POSTHOG_)"
      glob: ".env.example"
    
    - id: service-clients
      type: glob
      patterns: ["src/lib/*client.ts", "src/services/**/*.ts", "lib/services/**/*.ts"]
    
    - id: webhook-routes
      type: glob
      patterns: ["src/app/api/webhooks/**/*.ts", "pages/api/webhooks/**/*.ts"]
  
  verbosity: files_only
步骤 1.2:分析服务客户端模式

阅读现有服务客户端,了解项目的模式:

precision_read:
  files:
    - path: "src/lib/email-client.ts"
      extract: outline
    - path: "src/services/upload.ts"
      extract: symbols
  
  output:
    format: minimal

决策点: 新集成应采用现有模式。如果没有现成模式,请遵循以下实施指南。

阶段 2:服务选择

根据需求、规模和预算选择服务。

电子邮件服务决策树

用于事务性电子邮件:

  • Resend - 开发体验(DX)最佳,支持 React Email,每天免费发送 100 封,50K 封每月 $20
  • SendGrid - 企业级功能,每天免费发送 100 封,100K 封每月 $20
  • Postmark - 专注于高送达率,10K 封每月 $15,无免费套餐
  • AWS SES - 规模化后最便宜(每 1000 封 $0.10),需要更多设置

用于营销电子邮件:

  • ConvertKit - 面向创作者,1K 名订阅者每月 $29
  • Mailchimp - 一体化平台,500 名订阅者以内免费
  • Loops - 对开发者友好,2K 名订阅者每月 $49

建议: 事务性电子邮件从 Resend 起步,达到每月 1M 封以上后迁移到 SES。

CMS 平台决策树

用于结构化内容(博客、文档):

  • Sanity - 开发体验最佳,支持实时协作,免费套餐慷慨
  • Contentful - 可用于企业级场景,GraphQL 功能强大,定价复杂
  • Payload - 可自托管,完整支持 TypeScript,无供应商锁定

用于应用内容(产品、用户生成内容):

  • Payload - 最适合复杂数据模型,内置身份验证
  • Strapi - 插件生态丰富,可自托管

用于营销页面:

  • Builder.io - 可视化编辑器,内置 A/B 测试
  • Sanity - 对开发者友好,可通过 Sanity Studio 进行可视化编辑

建议: 内容密集型网站使用 Sanity,应用后端使用 Payload。

文件上传决策树

用于图片(头像、产品照片):

  • UploadThing - 零配置,集成 Next.js,2GB 存储每月 $10
  • Cloudinary - 支持图片转换,免费套餐提供 25GB 带宽
  • Vercel Blob - 边缘网络,存储每 GB $0.15

用于大型文件(视频、文档):

  • AWS S3 - 行业标准,存储每 GB $0.023,规模化后最便宜
  • Cloudflare R2 - 兼容 S3,零出口流量费用,存储每 GB $0.015
  • Backblaze B2 - 存储最便宜,每 GB $0.005

用于面向用户且需要病毒扫描的上传:

  • UploadThing - 内置病毒扫描
  • AWS S3 + Lambda - 使用 ClamAV 自行实现扫描

建议: 原型使用 UploadThing,生产规模场景使用 S3/R2。

分析决策树

用于产品分析:

  • PostHog - 可自托管选项、会话回放、功能标记,免费套餐包含 1M 个事件
  • Mixpanel - 以用户为中心的分析,免费套餐每月包含 20M 个事件
  • Amplitude - 高级群组分析,免费套餐每月包含 10M 个事件

用于网站分析:

  • Plausible - 注重隐私、符合 GDPR,10K 次页面浏览量每月 $9
  • Umami - 可自托管、简单、开源
  • Vercel Analytics - 专注于 Web Vitals,Vercel 项目可免费使用

建议: 产品应用使用 PostHog,营销网站使用 Plausible。

阶段 3:电子邮件集成

步骤 3.1:安装 Resend SDK
precision_exec:
  commands:
    - cmd: "npm install resend"
      expect:
        exit_code: 0
  
  verbosity: minimal
步骤 3.2:创建电子邮件客户端

编写带有错误处理的类型安全电子邮件客户端:

precision_write:
  files:
    - path: "src/lib/email.ts"
      mode: fail_if_exists
      content: |
        import type { ReactElement } from 'react';
        import { Resend } from 'resend';
        
        if (!process.env.RESEND_API_KEY) {
          throw new Error('RESEND_API_KEY is required');
        }
        
        export const resend = new Resend(process.env.RESEND_API_KEY);
        
        export interface SendEmailOptions {
          to: string | string[];
          subject: string;
          html?: string;
          react?: ReactElement;
          from?: string;
        }
        
        export async function sendEmail(options: SendEmailOptions) {
          const { to, subject, html, react, from = '[email protected]' } = options;
          
          try {
            const { data, error } = await resend.emails.send({
              from,
              to,
              subject,
              html,
              react,
            });
            
            if (error) {
              console.error('[Email] Send failed:', error);
              throw new Error(`Email send failed: ${error.message}`);
            }
            
            console.log('[Email] Sent successfully:', data?.id);
            return { success: true, id: data?.id };
          } catch (error: unknown) {
            console.error('[Email] Unexpected error:', error);
            throw error;
          }
        }
  
  verbosity: minimal
步骤 3.3:创建 React Email 模板

对于事务性电子邮件,使用 React Email 创建类型安全的模板:

precision_exec:
  commands:
    - cmd: "npm install react-email @react-email/components"
      expect:
        exit_code: 0
  
  verbosity: minimal
precision_write:
  files:
    - path: "emails/welcome.tsx"
      mode: fail_if_exists
      content: |
        import {
          Body,
          Button,
          Container,
          Head,
          Heading,
          Html,
          Preview,
          Text,
        } from '@react-email/components';
        
        interface WelcomeEmailProps {
          userName: string;
          loginUrl: string;
        }
        
        export default function WelcomeEmail({ userName, loginUrl }: WelcomeEmailProps) {
          return (
            <Html>
              <Head />
              <Preview>Welcome to our platform!</Preview>
              <Body style={main}>
                <Container style={container}>
                  <Heading style={h1}>Welcome, {userName}!</Heading>
                  <Text style={text}>
                    We're excited to have you on board. Click the button below to get started.
                  </Text>
                  <Button href={loginUrl} style={button}>
                    Get Started
                  </Button>
                </Container>
              </Body>
            </Html>
          );
        }
        
        const main = { backgroundColor: '#f6f9fc', fontFamily: '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif' };
        const container = { margin: '0 auto', padding: '40px 20px' };
        const h1 = { color: '#1f2937', fontSize: '24px', fontWeight: 'bold', marginBottom: '20px' };
        const text = { color: '#4b5563', fontSize: '16px', lineHeight: '24px', marginBottom: '20px' };
        const button = { backgroundColor: '#3b82f6', borderRadius: '6px', color: '#ffffff', display: 'inline-block', fontSize: '16px', fontWeight: '600', padding: '12px 24px', textDecoration: 'none' };
  
  verbosity: minimal
步骤 3.4:记录环境变量
precision_write:
  files:
    - path: ".env.example"
      mode: overwrite
      content: |
        # Email (Resend)
        RESEND_API_KEY=your_resend_api_key_here
  
  verbosity: minimal

阶段 4:CMS 集成

步骤 4.1:安装 Sanity SDK
precision_exec:
  commands:
    - cmd: "npm install @sanity/client @sanity/image-url"
      expect:
        exit_code: 0
  
  verbosity: minimal
步骤 4.2:创建 Sanity 客户端
precision_write:
  files:
    - path: "src/lib/sanity.ts"
      mode: fail_if_exists
      content: |
        import { createClient } from '@sanity/client';
        import imageUrlBuilder from '@sanity/image-url';
        import type { SanityImageSource } from '@sanity/image-url/lib/types/types';
        
        if (!process.env.NEXT_PUBLIC_SANITY_PROJECT_ID) {
          throw new Error('NEXT_PUBLIC_SANITY_PROJECT_ID is required');
        }
        
        if (!process.env.NEXT_PUBLIC_SANITY_DATASET) {
          throw new Error('NEXT_PUBLIC_SANITY_DATASET is required');
        }
        
        export const sanityClient = createClient({
          projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
          dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
          apiVersion: '2026-01-01', // Update to current API version date
          useCdn: process.env.NODE_ENV === 'production',
        });
        
        const builder = imageUrlBuilder(sanityClient);
        
        // Import from @sanity/image-url
        export function urlForImage(source: SanityImageSource) {
          return builder.image(source).auto('format').fit('max');
        }
        
        // Type-safe query helper
        export async function sanityFetch<T>(query: string, params?: Record<string, unknown>): Promise<T> {
          try {
            const result = await sanityClient.fetch<T>(query, params);
            return result;
          } catch (error: unknown) {
            console.error('[Sanity] Query failed:', error);
            throw new Error('Failed to fetch from Sanity');
          }
        }
  
  verbosity: minimal
步骤 4.3:设置 Webhook 端点

如需实时更新内容,请实现 Webhook 处理程序:

precision_write:
  files:
    - path: "src/app/api/webhooks/sanity/route.ts"
      mode: fail_if_exists
      content: |
        import { NextRequest, NextResponse } from 'next/server';
        import { revalidateTag } from 'next/cache';
        
        import { timingSafeEqual } from 'crypto';

        export async function POST(request: NextRequest) {
          const signature = request.headers.get('sanity-webhook-signature');
          
          // Verify webhook signature
          const secret = process.env.SANITY_WEBHOOK_SECRET;
          if (!secret) {
            return NextResponse.json({ error: 'Webhook secret not configured' }, { status: 500 });
          }
          const expected = Buffer.from(secret);
          const received = Buffer.from(signature || '');
          if (expected.length !== received.length || !timingSafeEqual(expected, received)) {
            return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
          }
          
          try {
            const body = await request.json();
            const { _type } = body;
            
            // Revalidate cache based on content type
            if (_type === 'post') {
              revalidateTag('posts');
            } else if (_type === 'page') {
              revalidateTag('pages');
            }
            
            console.log('[Webhook] Sanity content updated:', _type); // Note: Use structured logger in production
            return NextResponse.json({ revalidated: true });
          } catch (error: unknown) {
            console.error('[Webhook] Failed to process Sanity webhook:', error);
            return NextResponse.json({ error: 'Webhook processing failed' }, { status: 500 });
          }
        }
  
  verbosity: minimal

阶段 5:文件上传集成

步骤 5.1:安装 UploadThing SDK
precision_exec:
  commands:
    - cmd: "npm install uploadthing @uploadthing/react"
      expect:
        exit_code: 0
  
  verbosity: minimal
步骤 5.2:创建上传路由器
precision_write:
  files:
    - path: "src/app/api/uploadthing/core.ts"
      mode: fail_if_exists
      content: |
        import { createUploadthing, type FileRouter } from 'uploadthing/next';
        
        const f = createUploadthing();
        
        export const ourFileRouter = {
          imageUploader: f({ image: { maxFileSize: '4MB', maxFileCount: 4 } })
            .middleware(async ({ req }) => {
              // Authenticate user (placeholder imports shown for context)
              // In real code: import { getUserFromRequest } from '@/lib/auth';
              const user = await getUserFromRequest(req); // Import from your auth module
              if (!user) throw new Error('Unauthorized');
              
              return { userId: user.id };
            })
            .onUploadComplete(async ({ metadata, file }) => {
              console.log('[Upload] Complete:', file.url);
              
              // Save to database (Assumes Prisma client or similar ORM)
              await db.image.create({
                data: {
                  url: file.url,
                  userId: metadata.userId,
                },
              });
              
              return { url: file.url };
            }),
          
          pdfUploader: f({ pdf: { maxFileSize: '16MB' } })
            .middleware(async ({ req }) => {
              const user = await getUserFromRequest(req);
              if (!user) throw new Error('Unauthorized');
              return { userId: user.id };
            })
            .onUploadComplete(async ({ metadata, file }) => {
              console.log('[Upload] PDF complete:', file.url);
              return { url: file.url };
            }),
        } satisfies FileRouter;
        
        export type OurFileRouter = typeof ourFileRouter;
  
  verbosity: minimal
步骤 5.3:替代方案 - S3 预签名 URL

如需更多控制,请使用带有预签名 URL 的 S3:

precision_exec:
  commands:
    - cmd: "npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner"
      expect:
        exit_code: 0
  
  verbosity: minimal
precision_write:
  files:
    - path: "src/lib/s3.ts"
      mode: fail_if_exists
      content: |
        import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
        import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
        
        // Validate S3 configuration
        if (!process.env.AWS_REGION || !process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY || !process.env.AWS_S3_BUCKET) {
          throw new Error('Missing required AWS S3 environment variables');
        }
        
        const s3Client = new S3Client({
          region: process.env.AWS_REGION,
          credentials: {
            accessKeyId: process.env.AWS_ACCESS_KEY_ID,
            secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
          },
        });
        
        export async function getUploadUrl(key: string, contentType: string) {
          const command = new PutObjectCommand({
            Bucket: process.env.AWS_S3_BUCKET,
            Key: key,
            ContentType: contentType,
          });
          
          // URL expires in 5 minutes
          const url = await getSignedUrl(s3Client, command, { expiresIn: 300 });
          return url;
        }
  
  verbosity: minimal

阶段 6:分析集成

步骤 6.1:安装 PostHog SDK
precision_exec:
  commands:
    - cmd: "npm install posthog-js"
      expect:
        exit_code: 0
  
  verbosity: minimal
步骤 6.2:创建分析提供程序
precision_write:
  files:
    - path: "src/providers/analytics.tsx"
      mode: fail_if_exists
      content: |
        'use client';
        
        import { useEffect } from 'react';
        import type { ReactNode } from 'react';
        import posthog from 'posthog-js';
        
        export function AnalyticsProvider({ children }: { children: ReactNode }) {
          useEffect(() => {
            if (process.env.NEXT_PUBLIC_POSTHOG_KEY) {
              posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
                api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://app.posthog.com',
                loaded: (posthog) => {
                  if (process.env.NODE_ENV === 'development') {
                    posthog.opt_out_capturing(); // Disabled in dev to avoid polluting analytics (re-enable for testing with posthog.opt_in_capturing())
                  }
                },
              });
            }
          }, []);
          
          return <>{children}</>;
        }
        
        // Helper for tracking events
        export function trackEvent(eventName: string, properties?: Record<string, unknown>) {
          if (typeof window !== 'undefined') {
            posthog.capture(eventName, properties);
          }
        }
  
  verbosity: minimal

阶段 7:错误处理模式

步骤 7.1:实现指数退避

对于有速率限制的服务,请实现重试逻辑:

precision_write:
  files:
    - path: "src/lib/retry.ts"
      mode: fail_if_exists
      content: |
        export interface RetryOptions {
          maxAttempts?: number;
          initialDelayMs?: number;
          maxDelayMs?: number;
          backoffMultiplier?: number;
        }
        
        export async function withRetry<T>(
          fn: () => Promise<T>,
          options: RetryOptions = {}
        ): Promise<T> {
          const {
            maxAttempts = 3,
            initialDelayMs = 1000,
            maxDelayMs = 10000,
            backoffMultiplier = 2,
          } = options;
          
          let lastError: Error = new Error('All retry attempts failed');
          
          for (let attempt = 1; attempt <= maxAttempts; attempt++) {
            try {
              return await fn();
            } catch (error: unknown) {
              lastError = error instanceof Error ? error : new Error(String(error));
              
              if (attempt === maxAttempts) {
                break;
              }
              
              const delayMs = Math.min(
                initialDelayMs * Math.pow(backoffMultiplier, attempt - 1),
                maxDelayMs
              );
              
              console.log(`[Retry] Attempt ${attempt}/${maxAttempts} failed, retrying in ${delayMs}ms`); // Note: Use structured logger in production
              await new Promise(resolve => setTimeout(resolve, delayMs));
            }
          }
          
          throw lastError;
        }
  
  verbosity: minimal
步骤 7.2:实现熔断器

使用熔断器防止故障级联:

precision_write:
  files:
    - path: "src/lib/circuit-breaker.ts"
      mode: fail_if_exists
      content: |
        export class CircuitBreaker {
          private failureCount = 0;
          private lastFailureTime: number | null = null;
          private state: 'closed' | 'open' | 'half-open' = 'closed';
          
          constructor(
            private readonly failureThreshold = 5,
            private readonly resetTimeoutMs = 60000
          ) {}
          
          async execute<T>(fn: () => Promise<T>): Promise<T> {
            if (this.state === 'open') {
              if (Date.now() - (this.lastFailureTime ?? 0) > this.resetTimeoutMs) {
                this.state = 'half-open';
              } else {
                throw new Error('Circuit breaker is open');
              }
            }
            
            try {
              const result = await fn();
              this.onSuccess();
              return result;
            } catch (error: unknown) {
              this.onFailure();
              throw error;
            }
          }
          
          private onSuccess() {
            this.failureCount = 0;
            this.state = 'closed';
          }
          
          private onFailure() {
            this.failureCount++;
            this.lastFailureTime = Date.now();
            
            if (this.failureCount >= this.failureThreshold) {
              this.state = 'open';
              console.error('[CircuitBreaker] Circuit opened due to repeated failures');
            }
          }
        }
  
  verbosity: minimal

阶段 8:测试服务集成

步骤 8.1:模拟服务响应

创建模拟实现以便测试:

precision_write:
  files:
    - path: "src/lib/__mocks__/email.ts"
      mode: fail_if_exists
      content: |
        import { SendEmailOptions } from '../email';
        
        const sentEmails: Array<SendEmailOptions & { id: string }> = [];
        
        export async function sendEmail(options: SendEmailOptions) {
          const id = `mock-${Date.now()}`;
          sentEmails.push({ ...options, id });
          return { success: true, id };
        }
        
        export function getSentEmails() {
          return sentEmails;
        }
        
        export function clearSentEmails() {
          sentEmails.length = 0;
        }
  
  verbosity: minimal
步骤 8.2:实现 Webhook 测试

在本地测试 Webhook 处理程序:

precision_write:
  files:
    - path: "scripts/test-webhook.sh"
      mode: fail_if_exists
      content: |
        #!/usr/bin/env bash
        set -euo pipefail
        
        # Requires: bash 4+
        # Test Sanity webhook locally
        
        echo "[INFO] Testing Sanity webhook..."
        if curl -X POST http://localhost:3000/api/webhooks/sanity \
          -H "Content-Type: application/json" \
          -H "sanity-webhook-signature: $SANITY_WEBHOOK_SECRET" \
          -d '{
            "_type": "post",
            "_id": "test-123",
            "title": "Test Post"
          }'; then
          echo "[PASS] Webhook test successful"
        else
          echo "[FAIL] Webhook test failed"
          exit 1
        fi
  
  verbosity: minimal

阶段 9:验证

运行验证脚本,确保服务已正确集成:

precision_exec:
  commands:
    - cmd: "bash plugins/goodvibes/skills/outcome/service-integration/scripts/validate-services.sh ."
      expect:
        exit_code: 0
  
  verbosity: standard

常见模式

环境变量验证

始终在启动时验证必需的环境变量:

const requiredEnvVars = ['RESEND_API_KEY', 'SANITY_PROJECT_ID'];

for (const envVar of requiredEnvVars) {
  if (!process.env[envVar]) {
    throw new Error(`Missing required environment variable: ${envVar}`);
  }
}
对外发请求进行速率限制

实现令牌桶速率限制:

export class RateLimiter {
  private tokens: number;
  private lastRefill: number;
  
  constructor(
    private readonly maxTokens: number,
    private readonly refillRatePerSecond: number
  ) {
    this.tokens = maxTokens;
    this.lastRefill = Date.now();
  }
  
  async acquire(): Promise<void> {
    this.refill();
    
    if (this.tokens < 1) {
      const waitMs = (1 - this.tokens) * (1000 / this.refillRatePerSecond);
      await new Promise(resolve => setTimeout(resolve, waitMs));
      this.refill();
    }
    
    this.tokens -= 1;
  }
  
  private refill() {
    const now = Date.now();
    const elapsedSeconds = (now - this.lastRefill) / 1000;
    const tokensToAdd = elapsedSeconds * this.refillRatePerSecond;
    
    this.tokens = Math.min(this.maxTokens, this.tokens + tokensToAdd);
    this.lastRefill = now;
  }
}

应避免的反模式

1. 硬编码 API 密钥

错误:

const resend = new Resend('re_abc123');

正确:

if (!process.env.RESEND_API_KEY) {
  throw new Error('RESEND_API_KEY is required');
}
const resend = new Resend(process.env.RESEND_API_KEY);
2. 未处理错误

错误:

const result = await resend.emails.send(options);
return result.data;

正确:

const { data, error } = await resend.emails.send(options);
if (error) {
  console.error('[Email] Send failed:', error);
  throw new Error(`Email send failed: ${error.message}`);
}
return data;
3. 同步外部调用

错误:

// Blocking user request
await sendEmail({ to: user.email, subject: 'Welcome' });
return res.json({ success: true });

可接受(适用于简单情况):

// Send email async
// Note: Fire-and-forget is only suitable for non-critical operations.
// For production: use a queue-based approach with retries (see BEST example below).
sendEmail({ to: user.email, subject: 'Welcome' })
  .catch(err => console.error('[Email] Failed:', err));
return res.json({ success: true });

最佳(可用于生产环境):

// Queue-based approach with retries
await emailQueue.add('welcome-email', {
  to: user.email,
  subject: 'Welcome'
});
return res.json({ success: true });
4. 未验证 Webhook

错误:

export async function POST(request: NextRequest) {
  const body = await request.json();
  // Process without verification
}

正确:

import { timingSafeEqual } from 'crypto';

export async function POST(request: NextRequest) {
  const signature = request.headers.get('webhook-signature');
  
  // Validate webhook secret
  const secret = process.env.WEBHOOK_SECRET;
  if (!secret) {
    return NextResponse.json({ error: 'Webhook secret not configured' }, { status: 500 });
  }
  
  const expected = Buffer.from(secret);
  const received = Buffer.from(signature || '');
  if (expected.length !== received.length || !timingSafeEqual(expected, received)) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
  }
  const body = await request.json();
  // Process
}

故障排除

电子邮件未发送
  1. 验证是否已设置 API 密钥:printf "%s\n" "${RESEND_API_KEY:0:5}..."(用于显示前 5 个字符的 bash 参数展开)
  2. 在提供商控制面板中检查 API 密钥权限
  3. 验证发件人域名是否已通过验证
  4. 在提供商日志中检查速率限制
  5. 启用调试日志:resend.setDebug(true)
CMS 内容未更新
  1. 验证 Webhook 端点是否可从公网访问
  2. 检查 Webhook 密钥是否匹配
  3. 使用 ngrok 在本地测试 Webhook
  4. 验证缓存重新验证是否正常工作
  5. 检查 CMS Webhook 日志
文件上传失败
  1. 验证文件大小是否在限制范围内
  2. 检查 CORS 配置
  3. 验证身份验证中间件
  4. 使用较小的文件进行测试
  5. 检查 S3 存储桶权限(如果使用 S3)
分析事件未被跟踪
  1. 验证是否已设置 API 密钥
  2. 检查广告拦截器是否阻止了请求
  3. 验证开发环境中是否已关闭退出跟踪选项
  4. 检查浏览器控制台中的错误
  5. 使用 PostHog 调试模式进行测试

相关技能

  • api-design - 类型安全的 API 层模式
  • authentication - 安全的服务访问模式
  • testing-strategy - 测试服务集成

成功标准

  • 已安装服务 SDK 并创建客户端
  • 已在 .env.example 中记录环境变量
  • 已实现带重试的错误处理
  • Webhook 端点已进行签名验证
  • 已配置出站请求的速率限制
  • 已提供用于测试的模拟实现
  • 源代码中没有硬编码的 API 密钥
  • 所有验证检查均已通过

更多资源

评论

支持 Markdown