积分处理器 credits-handler

这是一个关于如何构建和管理SaaS平台积分系统的完整指南。它详细介绍了积分系统的后端配置(添加积分类型、定价策略、套餐分配)、前端UI实现(购买积分组件、余额显示组件)以及核心后端操作(积分分配、扣减、余额检查)。关键词包括:积分系统、SaaS平台、前端开发、后端开发、用户余额管理、定价策略、React钩子、TypeScript。

前端开发 0 次安装 0 次浏览 更新于 3/1/2026

name: credits-handler description: 管理积分系统(分配、购买、使用)。在添加积分类型、配置定价或构建积分UI时使用。

积分处理器

此技能指导您完成整个积分系统,从后端配置到前端UI实现。

1. 配置

所有积分配置都位于 src/lib/credits/config.ts 中。

添加新的积分类型

  1. 定义类型:将新类型添加到 creditTypeSchema 枚举中。

    export const creditTypeSchema = z.enum([
      "image_generation",
      "video_generation",
      "your_new_credit_type" // 添加此项
    ]);
    
  2. 配置定价和元数据:向 creditsConfig 添加一个条目。

    your_new_credit_type: {
      name: "新积分名称",
      currency: "USD",
      minimumAmount: 10,
      // 选项A:固定阶梯定价
      slabs: [
        { from: 1, to: 100, pricePerUnit: 0.10 },
        { from: 101, to: 1000, pricePerUnit: 0.08 },
      ],
      // 选项B:动态计算器(例如,基于用户套餐)
      priceCalculator: (amount, userPlan) => {
         // 此处逻辑
         return amount * 0.1;
      }
    }
    
  3. 套餐分配(可选):在 onPlanChangeCredits 中定义订阅套餐时给予多少积分。

2. UI实现:购买积分

要让用户购买积分,请使用 useBuyCredits 钩子。此钩子处理价格计算(考虑套餐折扣)和生成结账URL。

关键钩子:useBuyCredits

位置src/lib/credits/useBuyCredits.ts

用法

import useBuyCredits from "@/lib/credits/useBuyCredits";
import { PlanProvider } from "@/lib/plans/getSubscribeUrl";

const { 
  price,            // 计算出的总价(数字 | 未定义)
  isLoading,        // 价格计算进行中
  error,            // 错误状态
  getBuyCreditsUrl  // 生成支付URL的函数
} = useBuyCredits(creditType, amount);

示例:构建定价卡片

以下是创建积分购买UI的模式,类似于 src/components/website/website-credits-section.tsx

"use client";

import { useState } from "react";
import useBuyCredits from "@/lib/credits/useBuyCredits";
import { PlanProvider } from "@/lib/plans/getSubscribeUrl";
import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react";

// 1. 定义您的套餐包
const PACKAGE = {
  credits: 100,
  name: "入门包",
};

export function BuyCreditsCard({ creditType }: { creditType: "image_generation" }) {
  const [provider] = useState(PlanProvider.STRIPE); // 或 LEMONSQUEEZY

  // 2. 调用钩子
  const { price, isLoading, error, getBuyCreditsUrl } = useBuyCredits(
    creditType,
    PACKAGE.credits
  );

  // 3. 处理购买
  const handleBuy = () => {
    const url = getBuyCreditsUrl(provider);
    window.location.href = url;
  };

  // 4. 渲染UI
  return (
    <div className="border p-4 rounded-lg">
      <h3>{PACKAGE.name}</h3>
      <div className="text-2xl font-bold">
        {isLoading ? (
          <Loader2 className="animate-spin" />
        ) : (
          `$${price?.toFixed(2) || "0.00"}`
        )}
      </div>
      
      <Button 
        onClick={handleBuy}
        disabled={isLoading || !price}
        className="w-full mt-4"
      >
        购买 {PACKAGE.credits} 积分
      </Button>
      
      {error && <p className="text-red-500 text-sm">{error.message}</p>}
    </div>
  );
}

3. UI实现:显示积分

要显示用户的当前余额,请使用 useCredits 钩子。

关键钩子:useCredits

位置src/lib/users/useCredits.ts

返回数据结构

const { 
  credits,    // Record<string, number> | undefined
              // 例如 { "image_generation": 100, "video_generation": 50 }
  isLoading,  // 布尔值
  error,      // 任何类型
  mutate      // SWR mutate 函数,用于刷新数据
} = useCredits();

用法示例

import useCredits from "@/lib/users/useCredits";

export function CreditBalance() {
  const { credits, isLoading } = useCredits();

  if (isLoading) return <div>加载中...</div>;

  return (
    <div>
      图片生成积分: {credits?.image_generation || 0}
    </div>
  );
}

4. 核心操作(后端)

这些函数在API路由和Webhook中使用。

分配积分(例如,在套餐变更时)

使用 allocatePlanCredits 在用户订阅/升级时给予积分。

  • 文件src/lib/credits/allocatePlanCredits.ts
  • 输入userId, planId, paymentId(用于幂等性)。
  • 行为:检查 onPlanChangeCredits 配置,并在适用时添加积分。

添加/扣除积分

要手动操作余额,请使用 src/lib/credits/recalculate.ts 中的辅助函数(例如,addCredits, deductCredits)。 注意:在添加积分时,请始终确保您有一个唯一的 paymentId 或交易参考号,以防止重复。

5. 检查余额(后端/通用)

在执行操作之前,请使用 canDeductCredits

import { canDeductCredits } from "@/lib/credits/credits";

// 检查用户是否有足够的积分
const hasBalance = canDeductCredits(
  "image_generation", 
  1, 
  user // 必须包含 { credits: { ... } }
);

if (!hasBalance) {
  throw new Error("积分不足");
}

6. 参考

有关数据库模式和架构的深入探讨,请参阅 reference.md