开发者指南
积分系统开发指南
架构概述
积分系统由几个关键组件组成:
- 积分管理:积分操作的核心逻辑
- 配额服务:资源消耗跟踪
- 数据库架构:积分存储和历史记录
- API 端点:积分管理接口
- 定时任务:自动积分续期
数据库架构
积分系统使用以下数据库表:
用户积分表
CREATE TABLE user_credits (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES user(id),
credits INTEGER NOT NULL DEFAULT 0,
monthly_credits INTEGER NOT NULL DEFAULT 0,
last_reset_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
积分交易表
CREATE TABLE credit_transactions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES user(id),
amount INTEGER NOT NULL,
type TEXT NOT NULL, -- 'earn', 'spend', 'refund', 'grant'
description TEXT,
metadata JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
核心服务
积分服务
主要的积分服务处理所有积分操作:
// src/lib/credits/credit-service.ts
import { db } from '@/server/db'
import { userCredits, creditTransactions } from '@/server/db/schema'
import { eq } from 'drizzle-orm'
import { v4 as uuidv4 } from 'uuid'
export class CreditService {
static async getUserCredits(userId: string) {
const [credits] = await db
.select()
.from(userCredits)
.where(eq(userCredits.userId, userId))
.limit(1)
return credits || null
}
static async addCredits(
userId: string,
amount: number,
type: 'earn' | 'grant' | 'refund',
description?: string,
metadata?: Record<string, any>
) {
return await db.transaction(async (tx) => {
// 更新用户积分
await tx
.update(userCredits)
.set({
credits: sql`credits + ${amount}`,
updatedAt: new Date()
})
.where(eq(userCredits.userId, userId))
// 记录交易
await tx.insert(creditTransactions).values({
id: uuidv4(),
userId,
amount,
type,
description,
metadata
})
})
}
static async spendCredits(
userId: string,
amount: number,
description?: string,
metadata?: Record<string, any>
) {
const userCredit = await this.getUserCredits(userId)
if (!userCredit || userCredit.credits < amount) {
throw new Error('积分不足')
}
return await db.transaction(async (tx) => {
// 扣除积分
await tx
.update(userCredits)
.set({
credits: sql`credits - ${amount}`,
updatedAt: new Date()
})
.where(eq(userCredits.userId, userId))
// 记录交易
await tx.insert(creditTransactions).values({
id: uuidv4(),
userId,
amount: -amount,
type: 'spend',
description,
metadata
})
})
}
}
配额服务
配额服务管理资源消耗:
// src/lib/quota/quota-service.ts
import { CreditService } from '@/lib/credits/credit-service'
import { CREDIT_COSTS } from '@/config/credits.config'
export class QuotaService {
static async checkQuota(userId: string, resource: string, amount = 1) {
const cost = CREDIT_COSTS[resource] * amount
const userCredits = await CreditService.getUserCredits(userId)
if (!userCredits || userCredits.credits < cost) {
return { allowed: false, cost, available: userCredits?.credits || 0 }
}
return { allowed: true, cost, available: userCredits.credits }
}
static async consumeQuota(
userId: string,
resource: string,
amount = 1,
metadata?: Record<string, any>
) {
const cost = CREDIT_COSTS[resource] * amount
await CreditService.spendCredits(
userId,
cost,
`使用了 ${amount} 个 ${resource}`,
{ resource, amount, ...metadata }
)
return { cost, remaining: await this.getRemainingCredits(userId) }
}
static async getRemainingCredits(userId: string) {
const userCredits = await CreditService.getUserCredits(userId)
return userCredits?.credits || 0
}
}
配置
积分成本在积分配置文件中配置:
// src/config/credits.config.ts
export const CREDIT_COSTS = {
'ai_chat_message': 1,
'file_upload': 2,
'api_call': 1,
'premium_feature': 5
} as const
export const CREDIT_LIMITS = {
FREE_TIER: 100,
PRO_TIER: 1000,
ENTERPRISE_TIER: 10000
} as const
export const MONTHLY_CREDIT_GRANTS = {
FREE_TIER: 100,
PRO_TIER: 1000,
ENTERPRISE_TIER: 10000
} as const
API 端点
获取用户积分
// src/app/api/credits/route.ts
export async function GET(request: NextRequest) {
const session = await auth.api.getSession({ headers: request.headers })
if (!session?.user) {
return NextResponse.json({ error: '未授权' }, { status: 401 })
}
const credits = await CreditService.getUserCredits(session.user.id)
const transactions = await CreditService.getTransactionHistory(
session.user.id,
10
)
return NextResponse.json({ credits, transactions })
}
授予积分(管理员)
export async function POST(request: NextRequest) {
const session = await auth.api.getSession({ headers: request.headers })
if (!session?.user?.role === 'admin') {
return NextResponse.json({ error: '禁止访问' }, { status: 403 })
}
const { userId, amount, description } = await request.json()
await CreditService.addCredits(userId, amount, 'grant', description)
return NextResponse.json({ success: true })
}
中间件集成
将积分检查集成到您的 API 中间件中:
// src/middleware/credit-check.ts
import { QuotaService } from '@/lib/quota/quota-service'
export async function creditCheckMiddleware(
userId: string,
resource: string,
amount = 1
) {
const quota = await QuotaService.checkQuota(userId, resource, amount)
if (!quota.allowed) {
throw new Error(`积分不足。需要:${quota.cost},可用:${quota.available}`)
}
return quota
}
定时任务
自动月度积分续期:
// src/server/cron/credit-renewal.ts
import { CreditService } from '@/lib/credits/credit-service'
import { MONTHLY_CREDIT_GRANTS } from '@/config/credits.config'
export async function renewMonthlyCredits() {
const users = await db.select().from(user).where(eq(user.banned, false))
for (const user of users) {
const grantAmount = MONTHLY_CREDIT_GRANTS[user.tier] || MONTHLY_CREDIT_GRANTS.FREE_TIER
await CreditService.addCredits(
user.id,
grantAmount,
'grant',
'月度积分续期'
)
}
}
使用示例
在 API 路由中
export async function POST(request: NextRequest) {
const session = await auth.api.getSession({ headers: request.headers })
// 处理前检查配额
const quota = await QuotaService.checkQuota(session.user.id, 'ai_chat_message')
if (!quota.allowed) {
return NextResponse.json(
{ error: '积分不足', required: quota.cost },
{ status: 402 }
)
}
// 处理请求
const result = await processAIChat(request)
// 成功处理后消耗积分
await QuotaService.consumeQuota(session.user.id, 'ai_chat_message')
return NextResponse.json(result)
}
在 React 组件中
// src/hooks/use-credits.ts
export function useCredits() {
const [credits, setCredits] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchCredits()
}, [])
const fetchCredits = async () => {
try {
const response = await fetch('/api/credits')
const data = await response.json()
setCredits(data.credits)
} catch (error) {
console.error('获取积分失败:', error)
} finally {
setLoading(false)
}
}
return { credits, loading, refetch: fetchCredits }
}
最佳实践
- 事务安全:始终对积分操作使用数据库事务
- 错误处理:为积分不足实现适当的错误处理
- 审计跟踪:保留详细的交易记录以便调试
- 速率限制:结合速率限制以获得额外保护
- 监控:为异常积分消耗模式设置警报
- 测试:为积分操作编写全面的测试
测试
// tests/unit/credit-service.test.ts
import { CreditService } from '@/lib/credits/credit-service'
describe('CreditService', () => {
it('应该正确添加积分', async () => {
const userId = 'test-user'
const initialCredits = await CreditService.getUserCredits(userId)
await CreditService.addCredits(userId, 100, 'grant', '测试授予')
const updatedCredits = await CreditService.getUserCredits(userId)
expect(updatedCredits.credits).toBe(initialCredits.credits + 100)
})
it('应该防止花费超过可用积分', async () => {
const userId = 'test-user'
await expect(
CreditService.spendCredits(userId, 999999, '测试花费')
).rejects.toThrow('积分不足')
})
})
这个积分系统为您的 SaaS 应用程序中管理用户配额和资源消耗提供了强大的基础。