logoPay4SaaS
消费管理

积分消费

积分系统用于 积分模式配额订阅模式。如果你使用的是无限订阅或买断制,可以跳过这一页。

三种积分类型

系统内部维护三种积分,消费时按优先级依次扣减:

类型来源是否过期消费优先级
subscription_credits配额订阅的月度配额每月重置最高
bonus_credits赠送/活动积分有过期时间
purchased_credits用户购买的积分包永不过期最低

消费顺序: 优先扣月度配额 → 再扣赠送积分 → 最后扣购买积分。

这个优先级对用户最友好——先用快要过期的,购买的积分留到最后。

useCredits Hook

import { useCredits } from '@/hooks/useCredits'

function CreditsDisplay() {
  const { balance, loading, refreshBalance, useCredit } = useCredits()

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

  return (
    <div>
      <p>可用积分: {balance.availableCredits}</p>
      <p>已购积分: {balance.purchasedCredits}</p>
      <p>已使用: {balance.usedCredits}</p>
    </div>
  )
}

balance 对象

{
  totalCredits: number,      // 累计获得的积分
  usedCredits: number,       // 已使用的积分
  availableCredits: number,  // 当前可用积分
  purchasedCredits: number,  // 已购积分余额
}

useCredit —— 消费积分

const { useCredit } = useCredits()

async function handleUse() {
  const result = await useCredit(
    'image_generation',      // 服务类型
    '生成了一张图片',           // 描述(可选)
    'img-456'                // 关联 ID(可选)
  )

  if (result.success) {
    console.log('剩余:', result.remainingCredits)
  } else {
    // 积分不足
    console.log(result.message)
  }
}

useCredit 内部调用的是 /api/service/use,和 useAccessuseService 走的是同一个后端接口。两者都可以用,区别是:

  • useService(来自 useAccess)—— 通用消费,会自动更新访问状态
  • useCredit(来自 useCredits)—— 侧重积分场景,会自动更新积分余额显示

刷新余额

购买成功后或需要手动刷新积分余额时:

const { refreshBalance, addCredits } = useCredits()

// 方式 1:从服务器重新拉取(精确)
await refreshBalance()

// 方式 2:本地立即增加(快速更新 UI)
addCredits(50)  // 本地余额 +50

服务端积分操作

在 API Route 或服务端逻辑中,可以直接使用 lib/payment/credits.ts 提供的函数:

import {
  addCreditsToUser,          // 添加购买积分
  addBonusCredits,           // 添加赠送积分(带过期时间)
  getTotalAvailableCredits,  // 获取总可用积分
  initSubscriptionCredits,   // 初始化订阅配额积分
  clearSubscriptionCredits,  // 清除订阅配额积分(取消订阅时)
} from '@/lib/payment/credits'

// 给用户添加 50 个购买积分
await addCreditsToUser(userId, 50)

// 给用户赠送 10 个积分,30 天后过期
import { getBonusExpiryDate } from '@/config/payment'
await addBonusCredits(userId, 10, getBonusExpiryDate(30))

// 查询总可用积分
const total = await getTotalAvailableCredits(userId)

赠送积分

赠送积分(bonus_credits)按批次管理,每批有独立的过期时间。适合用于注册奖励、活动赠送、订阅附赠等场景。

首页 Hero 区(components/landing/Hero.tsx)的 "Grant 10 Bonus" 按钮是前端赠送积分的完整示例。

配置过期天数:

config/payment.ts 中设置默认过期天数:

export const BONUS_CREDITS_EXPIRY_DAYS = 30  // 默认 30 天

系统自动赠送的场景:

  • 订阅激活时,如果商品 metadata 中配置了 bonus_credits,会自动赠送
  • 积分包购买时,如果 metadata 中配置了 bonus_credits,会随购买附赠

手动赠送(前端页面):

const { grantBonus } = useAccess()

// 赠送 10 个积分,默认 30 天过期
const res = await grantBonus(10)

// 赠送 50 个积分,7 天过期
const res = await grantBonus(50, 7)

// res.success / res.message

手动赠送(后端 API route / server action):

import { addBonusCredits } from '@/lib/payment/credits'
import { getBonusExpiryDate } from '@/config/payment'

// 赠送 20 个积分,30 天后过期
await addBonusCredits(userId, 20, getBonusExpiryDate(30))

// 赠送 50 个积分,7 天后过期,标记来源为活动
await addBonusCredits(userId, 50, getBonusExpiryDate(7), 'campaign')

过期机制: 数据库 cron 每 10 分钟自动扫描并标记过期批次,触发器自动重算 user_credits 余额。应用层在用户访问时也会兜底检查。

原子扣减

所有积分扣减操作都通过数据库 RPC consume_quota 原子执行,不存在并发扣超的问题。RPC 内部按优先级(subscription_credits → bonus_credits → purchased_credits)依次扣减,并自动记录消费日志。

On this page