Sistine Starter

积分系统

Sistine Starter 的核心积分计费系统完整指南

积分系统

积分系统是 Sistine Starter 的核心计费机制。用户通过购买订阅计划或一次性积分包获得积分,使用 AI 功能时消耗积分。本文档详细介绍积分系统的架构设计、核心 API、消耗规则和最佳实践。

系统架构

设计原则

积分系统遵循以下核心设计原则:

  1. 事务性操作: 所有积分变动都在数据库事务中完成,确保数据一致性
  2. 完全可审计: 每笔积分变动都记录在账本中,支持完整的审计追踪
  3. 原子性: 积分扣除和账本记录要么同时成功,要么同时失败
  4. 透明性: 用户可以查看详细的积分变动历史和原因

数据库表结构

积分系统依赖两个核心数据表:

1. user 表 (积分余额)

建表语句请参考 数据库 文档中的 user 表定义,下表列出了与积分相关的关键字段。

关键字段说明:

字段类型说明
creditsINTEGER用户当前可用积分余额,所有功能消耗都从这里扣除
planKeyTEXT当前订阅计划标识 (free, starter_monthly, pro_yearly 等)

2. creditLedger 表 (积分账本)

建表语句请参考 数据库 文档中的 creditLedger 表定义,下表列出了主要字段。

关键字段说明:

字段类型说明
deltaINTEGER积分变化量。正数表示增加,负数表示扣除。例如: +300 表示赠送, -10 表示消耗
reasonVARCHAR(64)变动原因标识(见下表)
paymentIdTEXT如果积分来自支付,记录对应的 payment.id
createdAtTIMESTAMP变动时间,用于审计和历史查询

常见的 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: 用户 ID
  • creditsNeeded: 所需积分数量

返回值:

  • 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: 用户 ID
  • amount: 要扣除的积分数量(正数)
  • reason: 扣除原因(如 "chat_usage", "image_generation")
  • referenceId (可选): 关联的记录 ID(如 chatMessage.id, generationHistory.id)

返回值:

字段类型说明
successboolean是否成功扣除
remainingCreditsnumber扣除后的剩余积分
errorstring?失败时的错误信息

使用示例:

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: 用户 ID
  • amount: 要增加的积分数量(正数)
  • reason: 增加原因(如 "refund", "registration_bonus", "admin_adjustment")
  • referenceId (可选): 关联的记录 ID

返回值:

字段类型说明
successboolean是否成功退还
remainingCreditsnumber退还后的总积分

使用示例:

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$291,000立即发放全部
Pro Monthly$9910,000立即发放全部

发放逻辑:

// Webhook 处理 checkout.completed 事件
if (plan.grantSchedule?.mode === "per_cycle") {
  // 立即发放全部积分
  await refundCredits(
    userId,
    plan.creditsPerCycle,
    "subscription_cycle",
    paymentId
  );
}

年付计划 (分期发放)

计划价格总积分发放方式
Starter Yearly$29012,000每月发放 1,000,共 12 次
Pro Yearly$990120,000每月发放 10,000,共 12 次

发放逻辑:

  1. 首次购买: 立即发放第一个月的积分
  2. 后续发放: 通过定时任务每月自动发放

配置示例 (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
小型积分包$5200pack_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.creditscreditLedger 的总和一致:

// 审计脚本示例
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 应用添加基于积分的计费功能。