配置指南

项目中AI图片生成相关的配置

写在前面

1. 模型配置

默认使用:Kie(他最便宜),第模型是 google/nano-banana-pro

可选模型:9个模型(Nano Banana Pro、Seedream 4、Z-Image Turbo、Flux 2 Flex、Gemini 3 Pro等)

支持4个provider:Replicate、Fal、Gemini、Kie

如果你有更好更便宜的供应商平台,可以更换,这里只是教程,中心思想:能最便宜就用最便宜的

| 平台 | 模型版本 | 定价详情 | 备注 | 官网链接 |

| :--- | :--- | :--- | :--- | :--- |

| Kie.ai(Kie) | Nano Banana Pro | 约 $0.09(1K/2K)– $0.12(4K)
(18 credits ≈ $0.09;24 credits ≈ $0.12) | 使用 credits 计费,有不同兑换率和批量折扣 | kie.ai |

| Replicate | Nano Banana / Nano Banana Pro | 标准分辨率:≈ $0.14–$0.15 / 张
4K:常在 $0.24 左右 / 张 | 按运行时间或硬件计费,部分模型按输入/输出计费 | replicate.com |

| Fal.ai(Fal) | Nano Banana(非 Pro) | 约 $0.039 / 张(1MP 标准化) | 平台托管不同版本,需区分普通版与 Pro 版 | - |

| Fal.ai(Fal) | Nano Banana Pro / Gemini 3 Pro | 约 $0.15 / 张(标准)
4K 为双倍费率 | - | - |

| Google / Gemini 官方 | Nano Banana Pro | 标准:$0.13–$0.14 / 张
4K:
$0.24 / 张 | 有免费配额、学生或促销渠道,价格会有变动 | - |

2. 注册用户默认积分

通过数据库配置表动态控制

配置项:

initial_credits_enabled: 是否启用(需要在admin设置中开启)

initial_credits_amount: 赠送积分数量(默认需配置)

initial_credits_valid_days: 有效期天数

initial_credits_description: 描述信息

3. 每次生成消耗积分

Text-to-image: 4积分、

Image-to-image: 6积分

4. Pricing套餐

一次性购买: $29.9(原价$39.9)→ 400积分,有效期1年

月付订阅: $24.9/月(原价$36.9)→ 400积分/月

年付订阅: $229.9/年(原价$299.9)→ 4800积分/年(相当于$19.2/月,省23%)

5. 图片压缩配置

最大尺寸:1920px

压缩质量:0.7(70%)

格式:JPEG

6. 生成超时设置

轮询间隔:5秒

生成超时:180秒(3分钟)

Prompt最大长度:2000字符

AI 生成图片上传 R2 入库的完整链路:

  1. 生成请求 (/api/ai/generate)

用户提交 prompt → 扣除积分 → 调用 AI provider (Replicate/Kie/Fal) → 创建 ai_task 记录

  1. 异步回调 (AI provider webhook)

AI 完成生成 → 回调 /api/ai/notify/{provider} → 更新 ai_task 的 taskResult

  1. 轮询查询 (/api/ai/query)

前端轮询查询任务状态 → 调用 provider.query() → 更新 ai_task 的 taskInfo 和 taskResult

  1. 展示结果 (image.tsx)

解析 taskInfo 提取图片 URL → 展示给用户

  1. 保存到 showcase (saveShowcase)

调用 /api/proxy/file 下载图片 → 调用 /api/upload 上传到 R2 → 保存 URL 到 showcase 表

Kie.ai(Kie) 配置启用AI生成图片

使用须知:/admin/settings/ai 未配置点击生成会有Toast提示

image

注册使用Kie.ai

访问 kie.ia

先登录,然后回到上面的页面,往下拉到下图,点击图中按钮获取API key:

image

点击Create New Key

image image image

下面以本地开发举例(前提已经配置完超级管理员,可以进入admin)

访问:http://localhost:3000/admin/settings/ai

拉到最下面,按下图配置(1为粘贴你上面复制的)

image

这时候AI图片就配置好了,但是还不能用,因为你没有积分(需要支付后才可以)

支付请阅读✨项目中关于Stripe支付相关配置

另外,注意项目跑起来后及时支付Kie.ai(Kie)账单费用,避免停机造成用户流失

AI 配置与前台生效机制分析

目录

  1. 支持的 AI 提供商

  2. 配置方式

  3. 前台生效机制

  4. 模型配置

  5. 工作流程

  6. 配置示例


支持的 AI 提供商

项目支持 4 个 AI 提供商,每个提供商有不同的功能和特点:

1. Kie (kie.ai)

  • 功能: 音乐生成、图片生成、视频生成

  • 配置文件: src/extensions/ai/kie.ts

  • 支持场景:

    • 音乐: 文本生成音乐 (V4, V5 模型)

    • 图片: 文本生成图片、图片生成图片

    • 视频: 文本生成视频、图片生成视频

  • 特性: 支持自定义存储 (Custom Storage)

2. Replicate (replicate.com)

  • 功能: 图片生成、视频生成

  • 配置文件: src/extensions/ai/replicate.ts

  • 支持场景:

    • 图片: 文本生成图片、图片生成图片

    • 视频: 文本生成视频、图片生成视频

  • 特性: 支持自定义存储、支持多种开源模型

3. Fal (fal.ai)

  • 功能: 图片生成、视频生成

  • 配置文件: src/extensions/ai/fal.ts

  • 支持场景:

    • 图片: 文本生成图片、图片生成图片

    • 视频: 文本生成视频、图片生成视频、视频生成视频

  • 特性: 支持自定义存储、队列系统

4. Gemini (Google AI)

  • 功能: 图片生成

  • 配置文件: src/extensions/ai/gemini.ts

  • 支持场景:

    • 图片: 文本生成图片、图片生成图片
  • 特性: 同步生成、自动上传到自定义存储


配置方式

环境变量配置

可以在 .env.development.env.example 中配置(但项目主要使用数据库配置):


# Kie 配置

KIE_API_KEY=your_kie_api_key

KIE_CUSTOM_STORAGE=true



# Replicate 配置

REPLICATE_API_TOKEN=r8_xxx

REPLICATE_CUSTOM_STORAGE=true



# Fal 配置

FAL_API_KEY=fal_xxx

FAL_CUSTOM_STORAGE=true



# Gemini 配置

GEMINI_API_KEY=AIza...

数据库配置(推荐)

项目使用数据库存储配置,通过后台管理界面配置:

配置表结构 (config 表):


{

  // Kie

  kie_api_key: string

  kie_custom_storage: 'true' | 'false'

  

  // Replicate

  replicate_api_token: string

  replicate_custom_storage: 'true' | 'false'

  

  // Fal

  fal_api_key: string

  fal_custom_storage: 'true' | 'false'

  

  // Gemini

  gemini_api_key: string

}

配置位置: src/shared/services/settings.ts (第 730-795 行)

配置优先级


数据库配置 > 环境变量配置

配置读取逻辑在 src/shared/models/config.ts:


export async function getAllConfigs(): Promise<Configs> {

  // 1. 先读取环境变量

  const envConfigs = getEnvConfigs();

  

  // 2. 再读取数据库配置(会覆盖环境变量)

  const dbConfigs = await getConfigs();

  

  // 3. 合并配置

  return { ...envConfigs, ...dbConfigs };

}

🔄 前台生效机制

1. 初始化流程


用户访问页面



前端调用 /api/ai/providers



后端读取配置 (getAllConfigs)



检查哪些 provider 配置了 API Key



返回可用的 providers 列表



前端根据可用 providers 过滤模型选项

2. Provider 检测逻辑

API 路由: src/app/api/ai/providers/route.ts


export async function GET(request: NextRequest) {

  const configs = await getAllConfigs();

  const availableProviders: string[] = [];



  // 检查每个 provider 是否配置了 API Key

  if (configs.kie_api_key) {

    availableProviders.push('kie');

  }

  

  if (configs.replicate_api_token) {

    availableProviders.push('replicate');

  }

  

  if (configs.fal_api_key) {

    availableProviders.push('fal');

  }

  

  if (configs.gemini_api_key) {

    availableProviders.push('gemini');

  }



  return { providers: availableProviders };

}

关键点:

  • 只要配置了对应的 API Key,该 provider 就会被认为是可用的

  • 不验证 API Key 的有效性(在实际调用时才验证)

3. 前端使用逻辑

图片生成器: src/shared/blocks/generator/image.tsx


// 1. 获取可用的 providers

const [availableProviders, setAvailableProviders] = useState<string[]>([]);



useEffect(() => {

  fetch('/api/ai/providers')

    .then(res => res.json())

    .then(data => {

      const providers = data.data.providers || [];

      setAvailableProviders(providers);

      

      // 设置默认 provider 和 model

      if (providers.length > 0) {

        const firstProvider = providers[0];

        setProvider(firstProvider);

        

        // 找到第一个可用的模型

        const availableModel = MODEL_OPTIONS.find(

          option => 

            option.scenes.includes(activeTab) && 

            availableProviders.includes(option.provider)

        );

        if (availableModel) {

          setModel(availableModel.value);

        }

      }

    });

}, []);



// 2. 过滤可用的模型

const filteredModels = MODEL_OPTIONS.filter(

  option => 

    option.scenes.includes(activeTab) && 

    option.provider === provider &&

    availableProviders.includes(option.provider)

);



// 3. 生成前检查

const handleGenerate = async () => {

  // 检查是否有可用的 providers

  if (availableProviders.length === 0) {

    toast.error('Please contact the administrator to configure AI models.');

    return;

  }

  

  // 检查当前 provider 是否可用

  if (!availableProviders.includes(provider)) {

    toast.error('Please contact the administrator to configure AI models.');

    return;

  }

  

  // 调用生成 API

  await fetch('/api/ai/generate', {

    method: 'POST',

    body: JSON.stringify({

      provider,

      mediaType,

      model,

      prompt,

      options,

      scene

    })

  });

};

4. 后端生成逻辑

API 路由: src/app/api/ai/generate/route.ts


export async function POST(request: Request) {

  const { provider, mediaType, model, prompt, options, scene } = await request.json();

  

  // 1. 获取 AI 服务

  const aiService = await getAIService();

  

  // 2. 获取指定的 provider

  const aiProvider = aiService.getProvider(provider);

  if (!aiProvider) {

    throw new Error('invalid provider');

  }

  

  // 3. 调用 provider 生成

  const result = await aiProvider.generate({ params });

  

  // 4. 保存任务到数据库

  await createAITask(newAITask);

  

  return result;

}

5. AI 服务初始化

服务文件: src/shared/services/ai.ts


export function getAIManagerWithConfigs(configs: Configs) {

  const aiManager = new AIManager();



  // 根据配置动态添加 providers

  if (configs.kie_api_key) {

    aiManager.addProvider(

      new KieProvider({

        apiKey: configs.kie_api_key,

        customStorage: configs.kie_custom_storage === 'true',

      })

    );

  }



  if (configs.replicate_api_token) {

    aiManager.addProvider(

      new ReplicateProvider({

        apiToken: configs.replicate_api_token,

        customStorage: configs.replicate_custom_storage === 'true',

      })

    );

  }



  if (configs.fal_api_key) {

    aiManager.addProvider(

      new FalProvider({

        apiKey: configs.fal_api_key,

        customStorage: configs.fal_custom_storage === 'true',

      })

    );

  }



  if (configs.gemini_api_key) {

    aiManager.addProvider(

      new GeminiProvider({

        apiKey: configs.gemini_api_key,

      })

    );

  }



  return aiManager;

}

关键机制:

  • AIManager 维护一个 providers 数组

  • 只有配置了 API Key 的 provider 才会被添加

  • 前端通过 getProvider(name) 获取特定 provider

  • 如果 provider 不存在,返回 undefined


🎨 模型配置

图片生成模型

配置文件: src/shared/blocks/generator/image.tsx (第 77-135 行)


const MODEL_OPTIONS = [

  // Kie 模型

  {

    value: 'nano-banana-pro',

    label: 'Nano Banana Pro',

    provider: 'kie',

    scenes: ['text-to-image', 'image-to-image'],

  },

  

  // Replicate 模型

  {

    value: 'google/nano-banana-pro',

    label: 'Nano Banana Pro',

    provider: 'replicate',

    scenes: ['text-to-image', 'image-to-image'],

  },

  {

    value: 'bytedance/seedream-4',

    label: 'Seedream 4',

    provider: 'replicate',

    scenes: ['text-to-image', 'image-to-image'],

  },

  

  // Fal 模型

  {

    value: 'fal-ai/nano-banana-pro',

    label: 'Nano Banana Pro',

    provider: 'fal',

    scenes: ['text-to-image'],

  },

  {

    value: 'fal-ai/nano-banana-pro/edit',

    label: 'Nano Banana Pro',

    provider: 'fal',

    scenes: ['image-to-image'],

  },

  {

    value: 'fal-ai/bytedance/seedream/v4/edit',

    label: 'Seedream 4',

    provider: 'fal',

    scenes: ['image-to-image'],

  },

  {

    value: 'fal-ai/z-image/turbo',

    label: 'Z-Image Turbo',

    provider: 'fal',

    scenes: ['text-to-image'],

  },

  {

    value: 'fal-ai/flux-2-flex',

    label: 'Flux 2 Flex',

    provider: 'fal',

    scenes: ['text-to-image'],

  },

  

  // Gemini 模型

  {

    value: 'gemini-3-pro-image-preview',

    label: 'Gemini 3 Pro Image Preview',

    provider: 'gemini',

    scenes: ['text-to-image', 'image-to-image'],

  },

];

视频生成模型

配置文件: src/shared/blocks/generator/video.tsx (第 71-168 行)


const MODEL_OPTIONS = [

  // Replicate 模型

  {

    value: 'google/veo-3.1',

    label: 'Veo 3.1',

    provider: 'replicate',

    scenes: ['text-to-video', 'image-to-video'],

  },

  {

    value: 'openai/sora-2',

    label: 'Sora 2',

    provider: 'replicate',

    scenes: ['text-to-video', 'image-to-video'],

  },

  

  // Fal 模型

  {

    value: 'fal-ai/kling-video/o1/text-to-video',

    label: 'Kling Video O1',

    provider: 'fal',

    scenes: ['text-to-video'],

  },

  {

    value: 'fal-ai/kling-video/o1/image-to-video',

    label: 'Kling Video O1',

    provider: 'fal',

    scenes: ['image-to-video'],

  },

  

  // Kie 模型

  {

    value: 'kling-video-o1',

    label: 'Kling Video O1',

    provider: 'kie',

    scenes: ['text-to-video', 'image-to-video'],

  },

];

模型选择逻辑


// 1. 根据场景和 provider 过滤模型

const availableModels = MODEL_OPTIONS.filter(

  option => 

    option.scenes.includes(currentScene) && 

    option.provider === selectedProvider &&

    availableProviders.includes(option.provider)

);



// 2. 用户切换场景时自动切换模型

const handleSceneChange = (newScene) => {

  setActiveTab(newScene);

  

  const availableModels = MODEL_OPTIONS.filter(

    option => 

      option.scenes.includes(newScene) && 

      option.provider === provider

  );

  

  if (availableModels.length > 0) {

    setModel(availableModels[0].value);

  }

};



// 3. 用户切换 provider 时自动切换模型

const handleProviderChange = (newProvider) => {

  setProvider(newProvider);

  

  const availableModels = MODEL_OPTIONS.filter(

    option => 

      option.scenes.includes(activeTab) && 

      option.provider === newProvider

  );

  

  if (availableModels.length > 0) {

    setModel(availableModels[0].value);

  }

};

🔄 工作流程

完整的 AI 生成流程


1. 用户打开生成器页面



2. 前端调用 GET /api/ai/providers



3. 后端检查配置,返回可用 providers: ['kie', 'fal']



4. 前端根据可用 providers 过滤模型列表

   - 只显示 provider 为 'kie' 或 'fal' 的模型



5. 用户选择 provider: 'kie'



6. 前端自动过滤并显示 kie 的模型



7. 用户选择模型: 'nano-banana-pro'



8. 用户输入 prompt 并点击生成



9. 前端调用 POST /api/ai/generate

   {

     provider: 'kie',

     mediaType: 'image',

     model: 'nano-banana-pro',

     prompt: '...',

     scene: 'text-to-image',

     options: { ... }

   }



10. 后端获取 AI 服务

    - getAIService() 读取配置

    - 初始化 AIManager

    - 添加配置了的 providers



11. 后端获取 kie provider

    - aiService.getProvider('kie')



12. 调用 kie provider 生成

    - kieProvider.generate({ params })

    - 调用 Kie API



13. 保存任务到数据库

    - createAITask(newAITask)



14. 返回任务 ID 给前端



15. 前端轮询查询任务状态

    - POST /api/ai/query { taskId }



16. 后端查询 provider 任务状态

    - kieProvider.query({ taskId })



17. 任务完成,返回结果

    - 图片 URL

    - 视频 URL

    - 音乐 URL

Custom Storage 流程

当启用 custom_storage 时:


1. AI Provider 生成内容



2. 获取 Provider 返回的临时 URL



3. 调用 saveFiles() 函数



4. 下载文件内容



5. 上传到自定义存储 (R2/S3)



6. 替换为自定义存储的 URL



7. 返回给用户

代码示例 (src/extensions/ai/kie.ts):


// 查询任务结果

const result = await queryImage({ taskId });



// 如果启用了自定义存储

if (taskStatus === AITaskStatus.SUCCESS && 

    images && 

    images.length > 0 && 

    this.configs.customStorage) {

  

  // 准备要保存的文件

  const filesToSave: AIFile[] = images.map((image, index) => ({

    url: image.imageUrl,

    contentType: 'image/png',

    key: `kie/image/${getUuid()}.png`,

    index: index,

    type: 'image',

  }));

  

  // 保存到自定义存储

  const uploadedFiles = await saveFiles(filesToSave);

  

  // 替换 URL

  uploadedFiles.forEach((file: AIFile) => {

    if (file && file.url && file.index !== undefined) {

      images[file.index].imageUrl = file.url;

    }

  });

}

📝 配置示例

场景 1: 只配置 Kie

数据库配置:


kie_api_key = "your_kie_key"

kie_custom_storage = "true"

前端效果:

  • Provider 选择器只显示: Kie

  • 模型选择器只显示 Kie 的模型:

    • Nano Banana Pro (图片)

    • Kling Video O1 (视频)

  • 可以生成: 图片、视频、音乐

场景 2: 配置 Kie + Fal

数据库配置:


kie_api_key = "your_kie_key"

kie_custom_storage = "true"

fal_api_key = "your_fal_key"

fal_custom_storage = "false"

前端效果:

  • Provider 选择器显示: Kie, Fal

  • 切换到 Kie 时显示 Kie 的模型

  • 切换到 Fal 时显示 Fal 的模型:

    • Nano Banana Pro (图片)

    • Z-Image Turbo (图片)

    • Flux 2 Flex (图片)

    • Kling Video O1 (视频)

  • Kie 生成的文件会保存到自定义存储

  • Fal 生成的文件使用 Fal 的 URL

场景 3: 配置所有 Providers

数据库配置:


kie_api_key = "your_kie_key"

kie_custom_storage = "true"

replicate_api_token = "your_replicate_token"

replicate_custom_storage = "true"

fal_api_key = "your_fal_key"

fal_custom_storage = "true"

gemini_api_key = "your_gemini_key"

前端效果:

  • Provider 选择器显示: Kie, Replicate, Fal, Gemini

  • 每个 provider 都有自己的模型列表

  • 用户可以自由选择任意 provider 和模型

  • 所有文件都保存到自定义存储

场景 4: 未配置任何 Provider

数据库配置:


(所有 API Key 都为空)

前端效果:

  • /api/ai/providers 返回空数组: []

  • 前端检测到 availableProviders.length === 0

  • 用户点击生成时显示错误:

    
    "Please contact the administrator to configure AI models."
  • 无法使用任何 AI 功能


🔍 关键代码位置

配置相关

  • 配置模型: src/shared/models/config.ts

  • 配置读取: src/shared/models/config.ts - getAllConfigs()

  • 配置定义: src/shared/services/settings.ts (第 730-795 行)

AI 服务

  • AI Manager: src/extensions/ai/index.ts

  • AI 服务初始化: src/shared/services/ai.ts

  • Kie Provider: src/extensions/ai/kie.ts

  • Replicate Provider: src/extensions/ai/replicate.ts

  • Fal Provider: src/extensions/ai/fal.ts

  • Gemini Provider: src/extensions/ai/gemini.ts

API 路由

  • 获取可用 Providers: src/app/api/ai/providers/route.ts

  • 生成内容: src/app/api/ai/generate/route.ts

  • 查询任务: src/app/api/ai/query/route.ts

前端组件

  • 图片生成器: src/shared/blocks/generator/image.tsx

  • 视频生成器: src/shared/blocks/generator/video.tsx

  • 模型选择器: src/shared/components/ai-elements/model-selector.tsx


💡 总结

配置生效规则

  1. Provider 可用性: 只要配置了对应的 API Key,该 provider 就可用

  2. 模型过滤: 前端根据可用 providers 自动过滤模型列表

  3. 动态切换: 用户切换 provider 或场景时,自动切换到可用的模型

  4. 错误提示: 如果没有配置任何 provider,显示友好的错误提示

优势

  • 灵活配置: 可以只配置需要的 providers

  • 动态加载: 根据配置动态初始化 providers

  • 用户友好: 自动过滤不可用的选项

  • 易于扩展: 添加新 provider 只需实现 AIProvider 接口

注意事项

  • 配置修改后需要重启服务器才能生效

  • API Key 的有效性在实际调用时才验证

  • Custom Storage 需要先配置存储服务 (R2/S3)

  • 不同 provider 的模型参数可能不同

一些支付和积分的问答

支付-如果是月付的话是会每个月扣钱然后加credits吗?

答: 是的,月付会每个月扣钱然后加 Credits。

从代码分析来看,项目确实支持订阅续费时自动扣款并发放 Credits。项目支持两种支付类型:

  • one_time: 一次性付款

  • subscription: 订阅付款(包括月付、年付等)

总结

| 场景 | 扣款 | Credits 发放 | Credits 过期时间 |

|--------------|--------------------|---------------------------|-------------------------------|

| 首次订阅 | 立即扣款 | 立即发放 creditsAmount | 当前订阅周期结束(currentPeriodEnd) |

| 订阅续费(月付/年付) | 每个周期自动扣款 | 每次续费成功后自动发放 | 新的订阅周期结束 |

| 一次性付款 | 立即扣款 | 立即发放 | 按 creditsValidDays 计算 |

数据表结构

  • subscription 表:存储订阅信息,包括 creditsAmount、creditsValidDays、interval(月/年)、currentPeriodStart、currentPeriodEnd

  • credit 表:存储每次发放/消费的 Credits 记录,包括 remainingCredits、expiresAt

  • order 表:每次支付(包括续费)都会创建订单记录

简而言之:月付确实是每个月自动扣款,成功后系统会自动发放新的 Credits,这些 Credits 在下个扣款周期结束时过期。

Stripe、Creem、Paypal等支付都是这样的么?

答: 是的,Stripe、Creem、PayPal 都是统一的处理逻辑

项目采用了统一的支付抽象层设计,所有支付提供商都实现了相同的 PaymentProvider接口。


┌─────────────────────────────────────────────────────────┐

│              统一的业务处理层 (payment.ts)                │

│  handleCheckoutSuccess / handleSubscriptionRenewal      │

└─────────────────────────────────────────────────────────┘



                           │ 统一的 PaymentSession

┌─────────────────────────────────────────────────────────┐

│                 Webhook 路由 (notify/[provider])         │

│     根据 provider 参数选择对应的支付提供商处理              │

└─────────────────────────────────────────────────────────┘



         ┌─────────────────┼─────────────────┐

         ▼                 ▼                 ▼

┌────────────────┐ ┌────────────────┐ ┌────────────────┐

│ StripeProvider │ │ CreemProvider  │ │ PayPalProvider │

│  mapStripeEvent│ │  mapCreemEvent │ │   (暂未完善)   │

└────────────────┘ └────────────────┘ └────────────────┘

各提供商的事件映射

| 统一事件类型 | Stripe 原始事件 | Creem 原始事件 |

|--------------------|----------------------------------|-------------------------|

| CHECKOUT_SUCCESS | checkout.session.completed | checkout.completed |

| PAYMENT_SUCCESS | invoice.payment_succeeded | subscription.paid |

| SUBSCRIBE_UPDATED | customer.subscription.updated | subscription.update |

| SUBSCRIBE_CANCELED | customer.subscription.deleted | subscription.canceled |

关键点

  1. 统一处理逻辑:无论是 Stripe 还是 Creem,收到续费成功的 webhook 时:

    • 被映射为 PaymentEventType.PAYMENT_SUCCESS

    • subscriptionCycleType === SubscriptionCycleType.RENEWAL

    • 都会调用 ts handleSubscriptionRenewal() 发放新的 Credits

  2. PayPal 目前不完整:从代码看,PayPal 的 ts getPaymentEvent 返回的 paymentSessionundefined,说明 PayPal 的订阅续费功能尚未实现完整。

  3. Credits 发放时机

    • 首次订阅:CHECKOUT_SUCCESSts handleCheckoutSuccess()

    • 续费:PAYMENT_SUCCESS + RENEWALts handleSubscriptionRenewal()

结论

Stripe 和 Creem 的月付/年付逻辑完全一致:

  • ✅ 每个周期自动扣款

  • ✅ 扣款成功后自动发放 Credits

  • ✅ Credits 在当前周期结束时过期

PayPal 的订阅续费功能目前未完全实现,只有一次性支付和首次订阅可用。