阅读时间 3 分钟最后更新: 2025-12-31 12:12

Stripe 到 Creem 渐进迁移方案

本文档描述了将现有 Stripe 订阅用户渐进迁移到 Creem 支付平台的完整方案。采用渐进迁移策略:新用户使用 Creem,Stripe 老用户订阅到期后自动引导到 Creem 重新订阅,保留用户权益。

迁移策略

核心原则

  1. 新用户优先 Creem:所有新订阅默认使用 Creem 支付网关
  2. 老用户自然过渡:Stripe 订阅到期后不续费,引导用户在 Creem 重新订阅
  3. 权益保障:迁移期间保留用户的积分和订阅权益
  4. 平滑体验:通过 UI 提示和邮件通知,让用户了解迁移进度

迁移时间线

阶段 1 (立即): 配置变更
├── 将默认支付网关改为 Creem
├── 新用户自动使用 Creem
└── 现有 Stripe 订阅继续正常运行

阶段 2 (1-2周): UI 更新
├── 在订阅管理页面显示迁移提示
├── 添加迁移引导组件
└── 更新多语言文案

阶段 3 (持续): 自然迁移
├── Stripe 订阅到期后不续费
├── 用户访问时显示重新订阅引导
└── 用户在 Creem 完成新订阅

阶段 4 (可选): 主动迁移
├── 发送邮件通知剩余 Stripe 用户
├── 提供迁移激励(如额外积分)
└── 设置最终截止日期

技术实现

环境变量配置

修改 .env 文件:

# 将默认支付网关改为 Creem
PAYMENT_GATEWAY=creem

# Creem 配置(确保已配置)
CREEM_API_KEY=your_creem_api_key
CREEM_WEBHOOK_SECRET=your_creem_webhook_secret

# Stripe 配置(保留,用于处理现有订阅)
STRIPE_SECRET_KEY=your_stripe_secret_key
STRIPE_WEBHOOK_SECRET=your_stripe_webhook_secret

数据库结构

现有数据库结构已支持多网关,无需修改:

  • user_subscriptions.paymentGateway - 区分 Stripe 和 Creem 订阅
  • payment_customers - 存储用户在各网关的客户 ID
  • subscription_plans - 包含 Stripe 和 Creem 的 Price ID

核心代码修改

订阅服务 - 添加迁移检测

// src/lib/services/subscription-service.ts

/**
 * 检查用户是否需要迁移(Stripe 订阅即将到期)
 */
async needsMigration(userId: string): Promise<{
  needsMigration: boolean;
  currentSubscription?: UserSubscription;
  daysUntilExpiry?: number;
}> {
  const subscription = await this.getCurrentSubscription(userId);
  
  if (!subscription) {
    return { needsMigration: false };
  }
  
  // 只有 Stripe 订阅需要迁移
  if (subscription.paymentGateway !== 'stripe') {
    return { needsMigration: false };
  }
  
  // 检查是否已设置周期结束取消
  if (!subscription.cancelAtPeriodEnd) {
    return { needsMigration: false };
  }
  
  const now = new Date();
  const periodEnd = subscription.currentPeriodEnd;
  const daysUntilExpiry = Math.ceil(
    (periodEnd.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
  );
  
  return {
    needsMigration: true,
    currentSubscription: subscription,
    daysUntilExpiry,
  };
}

新增 API 端点

// src/app/api/subscription/migration/route.ts

import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
import { headers } from 'next/headers';
import { subscriptionService } from '@/lib/services/subscription-service';

export async function GET(request: NextRequest) {
  const session = await auth.api.getSession({
    headers: await headers(),
  });

  if (!session?.user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  try {
    const migrationStatus = await subscriptionService.getMigrationStatus(
      session.user.id
    );
    const needsMigration = await subscriptionService.needsMigration(
      session.user.id
    );
    
    return NextResponse.json({
      ...migrationStatus,
      ...needsMigration,
    });
  } catch (error) {
    console.error('Error getting migration status:', error);
    return NextResponse.json(
      { error: 'Failed to get migration status' },
      { status: 500 }
    );
  }
}

// 启动迁移(设置 Stripe 订阅不续费)
export async function POST(request: NextRequest) {
  const session = await auth.api.getSession({
    headers: await headers(),
  });

  if (!session?.user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  try {
    const subscription = await subscriptionService.getCurrentSubscription(
      session.user.id
    );
    
    if (!subscription || subscription.paymentGateway !== 'stripe') {
      return NextResponse.json(
        { error: 'No Stripe subscription found' },
        { status: 400 }
      );
    }
    
    // 设置 Stripe 订阅在周期结束时取消
    await subscriptionService.cancelSubscription(session.user.id, true);
    
    return NextResponse.json({
      success: true,
      message: 'Migration initiated.',
      periodEnd: subscription.currentPeriodEnd,
    });
  } catch (error) {
    console.error('Error initiating migration:', error);
    return NextResponse.json(
      { error: 'Failed to initiate migration' },
      { status: 500 }
    );
  }
}

多语言文案

中文 (public/locales/zh-CN/subscription.json)

{
  "migration": {
    "title": "支付系统升级",
    "pendingMessage": "我们正在升级支付系统。您当前的订阅将继续正常运行,但我们建议您切换到新的支付方式以获得更好的体验。",
    "inProgressMessage": "您当前的订阅将在 {{days}} 天后到期。请使用新的支付系统重新订阅以继续享受高级功能。",
    "resubscribeButton": "立即重新订阅",
    "startMigration": "开始迁移",
    "migrationComplete": "迁移完成",
    "benefits": {
      "title": "新支付系统的优势:",
      "faster": "更快的结账流程",
      "morePayments": "支持更多支付方式",
      "betterSupport": "更好的客户支持"
    }
  }
}

英文 (public/locales/en/subscription.json)

{
  "migration": {
    "title": "Payment System Migration",
    "pendingMessage": "We're upgrading our payment system. Your current subscription will continue normally, but we recommend switching to our new payment provider for a better experience.",
    "inProgressMessage": "Your current subscription will expire in {{days}} days. Please resubscribe using our new payment system to continue enjoying premium features.",
    "resubscribeButton": "Resubscribe Now",
    "startMigration": "Start Migration",
    "migrationComplete": "Migration Complete",
    "benefits": {
      "title": "Benefits of the new payment system:",
      "faster": "Faster checkout process",
      "morePayments": "More payment methods supported",
      "betterSupport": "Better customer support"
    }
  }
}

用户体验流程

新用户流程

  1. 新用户访问定价页
  2. 选择订阅计划
  3. 创建 Creem Checkout
  4. 完成支付
  5. 订阅激活

老用户迁移流程

  1. Stripe 用户登录
  2. 检查订阅状态
    • 活跃:显示迁移提示
    • 即将到期:显示紧急迁移提示
    • 已过期:显示重新订阅引导
  3. 用户点击迁移
  4. 设置 Stripe 不续费
  5. 引导到定价页
  6. 用户选择计划
  7. 创建 Creem Checkout
  8. 完成支付
  9. 新订阅激活
  10. 保留原有积分

注意事项

权益保障

  • 用户的积分余额在迁移过程中保持不变
  • 如果用户在 Stripe 订阅到期前完成 Creem 订阅,两个订阅可以并存
  • 建议在 Stripe 订阅到期后再激活 Creem 订阅的积分发放

Webhook 处理

  • 保持 Stripe Webhook 端点运行,处理现有订阅的事件
  • 确保 Creem Webhook 端点正常工作
  • 两个 Webhook 可以并行运行

客户支持

  • 准备 FAQ 文档解答用户疑问
  • 提供手动迁移支持渠道
  • 监控迁移过程中的错误和投诉

回滚计划

如果迁移出现问题:

  1. PAYMENT_GATEWAY 改回 stripe
  2. 恢复 Stripe 订阅的自动续费
  3. 通知受影响的用户

实施检查清单

  • 确认 Creem API 密钥和 Webhook Secret 已配置
  • 确认 Creem 产品/价格 ID 已在 subscription_plans 表中配置
  • 测试 Creem Checkout 流程
  • 测试 Creem Webhook 处理
  • 更新环境变量 PAYMENT_GATEWAY=creem
  • 部署迁移提示组件
  • 添加多语言文案
  • 运行迁移脚本(dry-run 模式)
  • 确认迁移脚本结果
  • 执行实际迁移
  • 监控迁移进度
  • 处理用户反馈