积分系统
Sistine Starter 的核心积分计费系统完整指南
积分系统
积分系统是 Sistine Starter 的核心计费机制。用户通过购买订阅计划或一次性积分包获得积分,使用 AI 功能时消耗积分。本文档详细介绍积分系统的架构设计、核心 API、消耗规则和最佳实践。
系统架构
设计原则
积分系统遵循以下核心设计原则:
- 事务性操作: 所有积分变动都在数据库事务中完成,确保数据一致性
- 完全可审计: 每笔积分变动都记录在账本中,支持完整的审计追踪
- 原子性: 积分扣除和账本记录要么同时成功,要么同时失败
- 透明性: 用户可以查看详细的积分变动历史和原因
数据库表结构
积分系统依赖两个核心数据表:
1. user
表 (积分余额)
建表语句请参考 数据库
文档中的 user
表定义,下表列出了与积分相关的关键字段。
关键字段说明:
字段 | 类型 | 说明 |
---|---|---|
credits | INTEGER | 用户当前可用积分余额,所有功能消耗都从这里扣除 |
planKey | TEXT | 当前订阅计划标识 (free , starter_monthly , pro_yearly 等) |
2. creditLedger
表 (积分账本)
建表语句请参考 数据库
文档中的 creditLedger
表定义,下表列出了主要字段。
关键字段说明:
字段 | 类型 | 说明 |
---|---|---|
delta | INTEGER | 积分变化量。正数表示增加,负数表示扣除。例如: +300 表示赠送, -10 表示消耗 |
reason | VARCHAR(64) | 变动原因标识(见下表) |
paymentId | TEXT | 如果积分来自支付,记录对应的 payment.id |
createdAt | TIMESTAMP | 变动时间,用于审计和历史查询 |
常见的 reason
值:
Reason | 说明 | Delta 符号 |
---|---|---|
registration_bonus | 新用户注册赠送 | 正数 (+300) |
subscription_cycle | 订阅周期发放 | 正数 |
one_time_pack | 购买一次性积分包 | 正数 |
admin_adjustment | 管理员手动调整 | 正数或负数 |
chat_usage | 对话功能消耗 | 负数 (-10) |
image_generation | 图像生成消耗 | 负数 (-20) |
video_generation | 视频生成消耗 | 负数 (-50) |
refund | 退款 | 正数 |
积分流转流程图
用户注册
↓
+300 积分 (registration_bonus)
↓
购买订阅 / 积分包
↓
+N 积分 (subscription_cycle / one_time_pack)
↓
使用 AI 功能
↓
-N 积分 (chat_usage / image_generation / video_generation)
↓
余额不足 → 购买更多积分
核心 API
核心积分操作函数位于 lib/credits.ts
,提供了完整的积分管理能力。
getUserCredits
查询用户当前可用积分余额。
函数签名:
async function getUserCredits(userId: string): Promise<number>
参数:
userId
: 用户 ID
返回值:
number
: 用户当前积分余额。如果用户不存在,返回0
使用示例:
import { getUserCredits } from "@/lib/credits";
// 查询用户积分
const credits = await getUserCredits("user_123");
console.log(`用户当前积分: ${credits}`);
应用场景:
- 在 UI 中显示用户积分余额
- 在执行付费操作前检查余额
- 管理后台查看用户积分
canUserAfford
检查用户是否有足够积分执行某个操作。
函数签名:
async function canUserAfford(
userId: string,
creditsNeeded: number
): Promise<boolean>
参数:
userId
: 用户 IDcreditsNeeded
: 所需积分数量
返回值:
boolean
:true
表示积分充足,false
表示余额不足
使用示例:
import { canUserAfford } from "@/lib/credits";
// 检查用户是否有足够积分生成视频 (需要 50 积分)
const canGenerate = await canUserAfford(userId, 50);
if (!canGenerate) {
return NextResponse.json(
{ error: "Insufficient credits", creditsNeeded: 50 },
{ status: 402 }
);
}
应用场景:
- API 路由中的前置检查
- 前端按钮禁用判断
- 防止用户在余额不足时调用昂贵操作
deductCredits
扣除用户积分并记录到账本。这是事务性操作,积分扣除和账本记录会同时成功或失败。
函数签名:
async function deductCredits(
userId: string,
amount: number,
reason: string,
referenceId?: string
): Promise<{
success: boolean;
remainingCredits: number;
error?: string;
}>
参数:
userId
: 用户 IDamount
: 要扣除的积分数量(正数)reason
: 扣除原因(如"chat_usage"
,"image_generation"
)referenceId
(可选): 关联的记录 ID(如chatMessage.id
,generationHistory.id
)
返回值:
字段 | 类型 | 说明 |
---|---|---|
success | boolean | 是否成功扣除 |
remainingCredits | number | 扣除后的剩余积分 |
error | string? | 失败时的错误信息 |
使用示例:
import { deductCredits } from "@/lib/credits";
// 扣除 20 积分用于图像生成
const result = await deductCredits(
userId,
20,
"image_generation",
imageHistoryId
);
if (!result.success) {
return NextResponse.json(
{
error: result.error,
remainingCredits: result.remainingCredits
},
{ status: 402 }
);
}
console.log(`扣除成功,剩余积分: ${result.remainingCredits}`);
核心实现逻辑 (简化版):
export async function deductCredits(
userId: string,
amount: number,
reason: string,
referenceId?: string
) {
// 1. 检查余额
const currentCredits = await getUserCredits(userId);
if (currentCredits < amount) {
return {
success: false,
remainingCredits: currentCredits,
error: "Insufficient credits"
};
}
// 2. 事务性操作:同时更新余额和账本
await db.transaction(async (tx) => {
// 更新用户积分余额
await tx.update(user)
.set({ credits: sql`${user.credits} - ${amount}` })
.where(eq(user.id, userId));
// 记录到账本 (delta 为负数)
await tx.insert(creditLedger).values({
id: randomUUID(),
userId,
delta: -amount, // 负数表示扣除
reason,
paymentId: referenceId,
});
});
// 3. 返回最新余额
const newCredits = await getUserCredits(userId);
return { success: true, remainingCredits: newCredits };
}
应用场景:
- AI 功能调用时扣除积分
- 付费功能的计费逻辑
- 任何需要消耗积分的操作
refundCredits
退还积分给用户并记录到账本。常用于失败重试、管理员调整或注册赠送。
函数签名:
async function refundCredits(
userId: string,
amount: number,
reason: string,
referenceId?: string
): Promise<{
success: boolean;
remainingCredits: number;
}>
参数:
userId
: 用户 IDamount
: 要增加的积分数量(正数)reason
: 增加原因(如"refund"
,"registration_bonus"
,"admin_adjustment"
)referenceId
(可选): 关联的记录 ID
返回值:
字段 | 类型 | 说明 |
---|---|---|
success | boolean | 是否成功退还 |
remainingCredits | number | 退还后的总积分 |
使用示例:
import { refundCredits } from "@/lib/credits";
// 新用户注册赠送 300 积分
await refundCredits(
newUser.id,
300,
"registration_bonus"
);
// AI 生成失败,退还积分
if (generationFailed) {
await refundCredits(
userId,
50,
"refund",
generationHistoryId
);
}
核心实现逻辑:
export async function refundCredits(
userId: string,
amount: number,
reason: string,
referenceId?: string
) {
await db.transaction(async (tx) => {
// 增加用户积分
await tx.update(user)
.set({ credits: sql`${user.credits} + ${amount}` })
.where(eq(user.id, userId));
// 记录到账本 (delta 为正数)
await tx.insert(creditLedger).values({
id: randomUUID(),
userId,
delta: amount, // 正数表示增加
reason,
paymentId: referenceId,
});
});
const newCredits = await getUserCredits(userId);
return { success: true, remainingCredits: newCredits };
}
应用场景:
- 新用户注册赠送积分
- 订阅计划周期性发放积分
- 管理员手动调整积分
- AI 生成失败后退款
积分消耗规则
当前消耗标准
不同 AI 功能的积分消耗如下:
功能 | 消耗积分 | 定义位置 |
---|---|---|
对话功能 | 10 积分/次 | lib/credits.ts (CHAT_CREDIT_COST) |
图像生成 | 20 积分/次 | app/api/image/generate/route.ts:36 |
视频生成 | 50 积分/次 | app/api/video/generate/route.ts:36 |
对话功能 (10 积分)
扣除时机: 每次发送消息给 AI 并收到回复时
实现示例 (app/api/chat/stream/route.ts
):
// 检查积分
const hasCredits = await canUserAfford(userId, 10);
if (!hasCredits) {
return new Response("Insufficient credits", { status: 402 });
}
// 扣除积分
const deductResult = await deductCredits(
userId,
10,
"chat_usage",
messageId
);
if (!deductResult.success) {
return new Response(deductResult.error, { status: 402 });
}
图像生成 (20 积分)
扣除时机: 提交图像生成请求时立即扣除
实现示例 (app/api/image/generate/route.ts
):
const creditsNeeded = 20;
// 1. 检查余额
const hasCredits = await canUserAfford(userId, creditsNeeded);
if (!hasCredits) {
return NextResponse.json({
error: "Insufficient credits",
creditsNeeded
}, { status: 402 });
}
// 2. 创建生成记录
const historyId = randomUUID();
await db.insert(generationHistory).values({
id: historyId,
userId,
type: "image",
prompt,
status: "processing",
creditsUsed: creditsNeeded,
});
// 3. 扣除积分
const result = await deductCredits(
userId,
creditsNeeded,
"image_generation",
historyId
);
if (!result.success) {
// 更新记录为失败
await db.update(generationHistory)
.set({ status: "failed", error: result.error })
.where(eq(generationHistory.id, historyId));
return NextResponse.json({ error: result.error }, { status: 402 });
}
// 4. 调用 AI 生成图像
try {
const image = await volcanoEngine.generateImage(prompt);
// ... 保存结果
} catch (error) {
// 生成失败,退还积分
await refundCredits(userId, creditsNeeded, "refund", historyId);
throw error;
}
视频生成 (50 积分)
扣除时机: 提交视频生成任务时立即扣除
实现逻辑: 与图像生成类似,但积分消耗更高 (50 积分)
特殊处理: 由于视频生成是异步任务,如果任务失败需要退还积分:
// 提交任务时扣除
await deductCredits(userId, 50, "video_generation", taskId);
// 定期检查任务状态
const taskStatus = await checkVideoStatus(taskId);
if (taskStatus.failed) {
// 失败则退还
await refundCredits(userId, 50, "refund", taskId);
}
积分获取方式
用户可以通过以下方式获得积分:
1. 注册赠送 (300 积分)
新用户注册时自动赠送 300 积分。
实现位置: lib/auth.ts:44-49
// Better Auth Hook: 用户注册后触发
hooks: {
after: createAuthMiddleware(async (ctx) => {
if (ctx.path.startsWith("/sign-up")) {
const newSession = ctx.context.newSession;
if (newSession) {
// 赠送 300 积分
await refundCredits(
newSession.user.id,
300,
"registration_bonus"
);
}
}
}),
}
账本记录:
userId: "user_xxx"
delta: +300
reason: "registration_bonus"
paymentId: null
2. 购买订阅计划
订阅计划按周期发放积分。所有计划配置位于 constants/billing.ts
。
月付计划 (立即发放)
计划 | 价格 | 积分/月 | 发放方式 |
---|---|---|---|
Starter Monthly | $29 | 1,000 | 立即发放全部 |
Pro Monthly | $99 | 10,000 | 立即发放全部 |
发放逻辑:
// Webhook 处理 checkout.completed 事件
if (plan.grantSchedule?.mode === "per_cycle") {
// 立即发放全部积分
await refundCredits(
userId,
plan.creditsPerCycle,
"subscription_cycle",
paymentId
);
}
年付计划 (分期发放)
计划 | 价格 | 总积分 | 发放方式 |
---|---|---|---|
Starter Yearly | $290 | 12,000 | 每月发放 1,000,共 12 次 |
Pro Yearly | $990 | 120,000 | 每月发放 10,000,共 12 次 |
发放逻辑:
- 首次购买: 立即发放第一个月的积分
- 后续发放: 通过定时任务每月自动发放
配置示例 (constants/billing.ts
):
starter_yearly: {
creditsPerCycle: 12000,
grantSchedule: {
mode: "installments",
grantsPerCycle: 12, // 分 12 次发放
intervalMonths: 1, // 每月发放一次
creditsPerGrant: 1000, // 每次发放 1000 积分
initialGrants: 1, // 首次立即发放 1 次
}
}
分期发放管理表 (subscriptionCreditSchedule
)
建表语句请参考 数据库
文档中的对应章节。
定时任务: app/api/cron/grant-subscription-credits/route.ts
- 运行频率: 每小时
- 触发条件:
nextGrantAt <= NOW() AND grantsRemaining > 0
- 操作: 发放积分并更新下次发放时间
3. 一次性积分包
用户可以随时购买积分包,立即到账。
当前可用积分包 (constants/billing.ts
):
积分包 | 价格 | 积分数 | Key |
---|---|---|---|
小型积分包 | $5 | 200 | pack_200 |
购买流程:
// 1. 用户完成支付
// 2. Webhook 处理 checkout.completed
const pack = oneTimePacks[packKey];
// 3. 立即发放积分
await refundCredits(
userId,
pack.credits,
"one_time_pack",
paymentId
);
4. 管理员手动调整
管理员可以通过后台为用户手动增减积分。
实现位置: app/api/admin/users/[userId]/credits/route.ts
// 管理员调整积分
export async function POST(req: NextRequest) {
// 验证管理员权限
if (currentUser.role !== "admin") {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const { amount, reason } = await req.json();
if (amount > 0) {
// 增加积分
await refundCredits(userId, amount, reason || "admin_adjustment");
} else {
// 扣除积分
await deductCredits(userId, Math.abs(amount), reason || "admin_adjustment");
}
}
应用场景:
- 用户申诉处理
- 活动奖励发放
- 补偿用户服务问题
- 测试账号初始化
在自己的功能中集成积分扣除
如果你要添加新的付费功能,按照以下步骤集成积分系统:
步骤 1: 定义积分消耗量
在你的 API 路由顶部定义消耗量:
// app/api/your-feature/route.ts
const FEATURE_CREDIT_COST = 15; // 你的功能消耗 15 积分
步骤 2: 前置检查用户余额
import { canUserAfford, deductCredits } from "@/lib/credits";
export async function POST(req: NextRequest) {
// 1. 认证用户
const session = await auth.api.getSession({ headers: req.headers });
if (!session?.session?.userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const userId = session.session.userId;
// 2. 检查积分余额
const hasCredits = await canUserAfford(userId, FEATURE_CREDIT_COST);
if (!hasCredits) {
return NextResponse.json(
{
error: "Insufficient credits",
creditsNeeded: FEATURE_CREDIT_COST
},
{ status: 402 }
);
}
// ... 继续处理
}
步骤 3: 执行操作前扣除积分
// 3. 扣除积分
const deductResult = await deductCredits(
userId,
FEATURE_CREDIT_COST,
"your_feature_usage", // 自定义原因标识
operationId // 关联的记录 ID
);
if (!deductResult.success) {
return NextResponse.json(
{ error: deductResult.error },
{ status: 402 }
);
}
// 4. 执行实际功能
try {
const result = await yourFeatureLogic();
return NextResponse.json({
result,
remainingCredits: deductResult.remainingCredits,
});
} catch (error) {
// 5. 失败时退还积分
await refundCredits(
userId,
FEATURE_CREDIT_COST,
"refund",
operationId
);
throw error;
}
完整示例: 添加"文档分析"功能
// app/api/analyze-document/route.ts
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { canUserAfford, deductCredits, refundCredits } from "@/lib/credits";
import { db } from "@/lib/db";
import { analysisHistory } from "@/lib/db/schema"; // 假设的表
import { randomUUID } from "crypto";
import { eq } from "drizzle-orm";
const ANALYSIS_CREDIT_COST = 25; // 文档分析消耗 25 积分
export async function POST(req: NextRequest) {
try {
// 1. 认证
const session = await auth.api.getSession({ headers: req.headers });
if (!session?.session?.userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const userId = session.session.userId;
// 2. 解析请求
const { documentUrl, analysisType } = await req.json();
if (!documentUrl) {
return NextResponse.json({ error: "Document URL required" }, { status: 400 });
}
// 3. 检查积分余额
const hasCredits = await canUserAfford(userId, ANALYSIS_CREDIT_COST);
if (!hasCredits) {
return NextResponse.json(
{
error: "Insufficient credits",
creditsNeeded: ANALYSIS_CREDIT_COST
},
{ status: 402 }
);
}
// 4. 创建分析记录
const analysisId = randomUUID();
await db.insert(analysisHistory).values({
id: analysisId,
userId,
documentUrl,
type: analysisType,
status: "pending",
creditsUsed: ANALYSIS_CREDIT_COST,
});
// 5. 扣除积分
const deductResult = await deductCredits(
userId,
ANALYSIS_CREDIT_COST,
"document_analysis", // 自定义原因
analysisId
);
if (!deductResult.success) {
// 更新分析记录为失败
await db.update(analysisHistory)
.set({ status: "failed", error: deductResult.error })
.where(eq(analysisHistory.id, analysisId));
return NextResponse.json(
{ error: deductResult.error },
{ status: 402 }
);
}
// 6. 执行实际分析
try {
const analysisResult = await performDocumentAnalysis(documentUrl);
// 更新分析记录
await db.update(analysisHistory)
.set({
status: "completed",
result: JSON.stringify(analysisResult),
updatedAt: new Date(),
})
.where(eq(analysisHistory.id, analysisId));
return NextResponse.json({
id: analysisId,
result: analysisResult,
remainingCredits: deductResult.remainingCredits,
});
} catch (error: any) {
// 7. 失败时退还积分
await refundCredits(userId, ANALYSIS_CREDIT_COST, "refund", analysisId);
// 更新记录
await db.update(analysisHistory)
.set({
status: "failed",
error: error.message,
updatedAt: new Date(),
})
.where(eq(analysisHistory.id, analysisId));
throw error;
}
} catch (error: any) {
console.error("Document analysis error:", error);
return NextResponse.json(
{ error: error.message || "Analysis failed" },
{ status: 500 }
);
}
}
调整积分消耗规则
修改现有功能的消耗量
对话功能
编辑 lib/credits.ts
:
// 将对话消耗从 10 调整为 5
const CHAT_CREDIT_COST = 5;
图像生成
编辑 app/api/image/generate/route.ts
:
// 第 36 行
const creditsNeeded = 30; // 从 20 改为 30
视频生成
编辑 app/api/video/generate/route.ts
:
// 第 36 行
const creditsNeeded = 100; // 从 50 改为 100
添加动态定价
你可以根据不同参数动态计算积分消耗:
// 根据视频时长动态定价
const baseCost = 50;
const durationMultiplier = {
"5s": 1,
"10s": 2,
"15s": 3,
};
const creditsNeeded = baseCost * (durationMultiplier[duration] || 1);
// 根据图像分辨率动态定价
const resolutionCost = {
"512x512": 20,
"1024x1024": 40,
"2048x2048": 80,
};
const creditsNeeded = resolutionCost[resolution] || 20;
会员折扣
为不同订阅等级提供折扣:
import { db } from "@/lib/db";
import { user as userTable } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
// 获取用户计划
const users = await db.select().from(userTable).where(eq(userTable.id, userId));
const userPlan = users[0]?.planKey || "free";
// 定义折扣
const discounts = {
free: 1.0, // 无折扣
starter_monthly: 0.9, // 9 折
starter_yearly: 0.85, // 85 折
pro_monthly: 0.8, // 8 折
pro_yearly: 0.7, // 7 折
};
const baseCost = 50;
const finalCost = Math.ceil(baseCost * discounts[userPlan]);
await deductCredits(userId, finalCost, "video_generation");
最佳实践
1. 始终使用事务
积分扣除和账本记录必须在同一事务中:
// ✅ 正确: deductCredits 内部使用事务
await deductCredits(userId, 10, "chat_usage");
// ❌ 错误: 分离操作可能导致不一致
await db.update(user).set({ credits: credits - 10 });
await db.insert(creditLedger).values({ delta: -10 });
2. 总是检查余额
即使前端已检查,后端必须再次验证:
// ✅ 正确: 后端验证
const hasCredits = await canUserAfford(userId, cost);
if (!hasCredits) {
return NextResponse.json({ error: "Insufficient credits" }, { status: 402 });
}
// ❌ 错误: 信任前端传来的数据
const { hasCredits } = await req.json(); // 不安全!
if (hasCredits) { ... }
3. 失败时退还积分
如果操作失败,必须退还已扣除的积分:
try {
const result = await expensiveOperation();
} catch (error) {
// ✅ 退还积分
await refundCredits(userId, cost, "refund", operationId);
throw error;
}
4. 提供清晰的错误信息
告诉用户还需要多少积分:
if (!hasCredits) {
const currentCredits = await getUserCredits(userId);
return NextResponse.json({
error: "Insufficient credits",
creditsNeeded: cost,
currentCredits,
shortfall: cost - currentCredits,
}, { status: 402 });
}
5. 使用有意义的 reason
账本的 reason
字段应该清晰描述变动原因:
// ✅ 正确: 清晰的原因标识
await deductCredits(userId, 20, "image_generation");
await refundCredits(userId, 300, "registration_bonus");
// ❌ 错误: 模糊的原因
await deductCredits(userId, 20, "usage");
await refundCredits(userId, 300, "bonus");
6. 记录关联 ID
使用 referenceId
参数关联业务记录:
const imageId = randomUUID();
// 创建生成记录
await db.insert(generationHistory).values({ id: imageId, ... });
// ✅ 扣除积分时关联记录 ID
await deductCredits(userId, 20, "image_generation", imageId);
// 这样可以在账本中追溯到具体的操作
7. 前端实时显示余额
在操作后更新前端显示的积分:
// API 响应
return NextResponse.json({
result: data,
remainingCredits: deductResult.remainingCredits, // ✅ 返回最新余额
});
// 前端更新
const response = await fetch("/api/chat", { ... });
const { result, remainingCredits } = await response.json();
setUserCredits(remainingCredits); // 更新 UI
8. 避免重复扣费
使用幂等性键防止重复扣费:
// 使用唯一的 operationId
const operationId = randomUUID(); // 或前端生成的 UUID
// 检查是否已存在该操作的记录
const existing = await db.select()
.from(generationHistory)
.where(eq(generationHistory.id, operationId));
if (existing.length > 0) {
return NextResponse.json({
error: "Operation already processed"
}, { status: 409 });
}
// 继续扣费逻辑...
9. 定期审计账本
确保 user.credits
与 creditLedger
的总和一致:
// 审计脚本示例
const ledgerSum = await db
.select({ total: sql`SUM(delta)` })
.from(creditLedger)
.where(eq(creditLedger.userId, userId));
const userCredits = await getUserCredits(userId);
if (ledgerSum[0].total !== userCredits) {
console.error(`Credits mismatch for user ${userId}`);
// 发送告警或自动修复
}
10. 为管理员提供审计工具
管理后台应该能够:
- 查看用户的积分变动历史
- 筛选特定原因的记录
- 导出账本数据
参考实现: app/[locale]/(admin)/admin/credits/page.tsx
常见问题
Q: 如何查看用户的积分历史?
import { db } from "@/lib/db";
import { creditLedger } from "@/lib/db/schema";
import { eq, desc } from "drizzle-orm";
const history = await db
.select()
.from(creditLedger)
.where(eq(creditLedger.userId, userId))
.orderBy(desc(creditLedger.createdAt))
.limit(50);
Q: 如何计算用户总共消耗了多少积分?
import { sql } from "drizzle-orm";
const totalSpent = await db
.select({ total: sql`SUM(ABS(delta))` })
.from(creditLedger)
.where(
sql`${creditLedger.userId} = ${userId} AND ${creditLedger.delta} < 0`
);
Q: 如何为所有用户批量发放积分?
// 批量发放 (谨慎使用)
const allUsers = await db.select({ id: user.id }).from(user);
for (const u of allUsers) {
await refundCredits(u.id, 100, "holiday_bonus");
}
Q: 用户删除账号时,积分记录会怎样?
积分账本记录会自动级联删除,因为 Schema 中定义了 ON DELETE CASCADE
:
"user_id" TEXT REFERENCES "user"("id") ON DELETE CASCADE
Q: 如何给特定用户组发放积分?
// 示例: 给所有 Pro 用户发放 1000 积分
const proUsers = await db
.select({ id: user.id })
.from(user)
.where(sql`${user.planKey} LIKE 'pro_%'`);
for (const u of proUsers) {
await refundCredits(u.id, 1000, "pro_member_bonus");
}
相关文档
总结
积分系统是 Sistine Starter 的核心计费机制,通过:
- 事务性操作确保数据一致性
- 完整账本实现审计追溯
- 灵活配置支持多种定价策略
- 清晰 API简化业务集成
遵循本文档的最佳实践,你可以安全、可靠地为你的 SaaS 应用添加基于积分的计费功能。