消费管理
积分消费
积分系统用于 积分模式 和 配额订阅模式。如果你使用的是无限订阅或买断制,可以跳过这一页。
三种积分类型
系统内部维护三种积分,消费时按优先级依次扣减:
| 类型 | 来源 | 是否过期 | 消费优先级 |
|---|---|---|---|
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,和 useAccess 的 useService 走的是同一个后端接口。两者都可以用,区别是:
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)依次扣减,并自动记录消费日志。