积分系统概述
了解 NEXTDEVKIT 中用于管理用户积分、消费和变现的完整积分系统
🎯 什么是积分系统?
NEXTDEVKIT 中的积分系统是一个灵活、生产就绪的变现框架,专为 AI 应用、API 服务和任何基于使用量计费的模式而设计。它提供了完整的解决方案:
- 💰 一次性积分购买 - 用户购买积分包
- 🎁 订阅积分 - 为订阅用户每月发放积分
- 🎉 注册奖励 - 为新用户提供欢迎积分
- 📊 使用追踪 - 精确的积分消耗监控
- ⏰ 过期管理 - 使用 FIFO 自动处理积分过期
- 🔄 自动发放 - 计划任务每月分发积分
🎨 为什么使用积分系统?
适合 AI 和 API 服务
如果您正在构建 AI 应用、API 平台或任何需要基于使用量计费的服务,积分系统提供:
- 灵活的变现方式:结合订阅和按需付费积分
- 用户自由度:让免费用户购买积分包,无需强制订阅
- 可预测的收入:混合经常性和一次性收入流
- 用户参与工具:注册奖励降低新用户的使用门槛
主要优势
- ✅ FIFO 过期机制:先进先出的公平积分消费
- ✅ 复式记账:准确的财务追踪和审计
- ✅ 自动化管理:定时任务处理发放和过期
- ✅ 支付集成:无缝集成 Stripe/Creem
- ✅ 类型安全:完整的 TypeScript 支持和 Drizzle ORM
- ✅ 生产就绪:经过实战检验,具有完善的错误处理
🏗️ 系统架构
积分流程
┌─────────────────────────────────────────────────────────────┐
│ 积分来源 │
├─────────────────────────────────────────────────────────────┤
│ • 注册奖励 • 积分包 • 订阅计划 │
└────────────────┬────────────────────────────────────────────┘
│
▼
┌────────────────────────┐
│ 积分批次 │
│ (FIFO 队列) │
│ │
│ ┌──────────────────┐ │
│ │ 批次 1: 100 积分 │ ← 最早(最先过期)
│ ├──────────────────┤ │
│ │ 批次 2: 200 积分 │
│ ├──────────────────┤ │
│ │ 批次 3: 50 积分 │ ← 最新
│ └──────────────────┘ │
└────────────┬───────────┘
│
▼
┌────────────────────────┐
│ 积分消费 │
│ (服务使用) │
└────────────────────────┘
数据库架构
系统使用三个主要表:
credits_balance
- 用户积分余额
- id: 用户余额 ID
- userId: 用户引用
- balance: 可用积分
- totalEarned: 累计获得积分
- totalSpent: 累计消费积分
- status: 账户状态 (ACTIVE/FROZEN)
credits_batch
- FIFO 积分批次
- id: 批次 ID
- userId: 用户引用
- amount: 原始积分数量
- remaining: 剩余积分
- issuedAt: 创建时间戳
- expiresAt: 过期日期 (可为空)
- status: ACTIVE/CONSUMED/EXPIRED
- sourceType: PURCHASE/SUBSCRIPTION/BONUS
- sourcePlan: 计划标识符
- grantPeriod: YYYY-MM 格式的月度发放周期
credits_transaction
- 交易历史
- id: 交易 ID
- userId: 用户引用
- type: purchase/consumption/monthly_grant/registration_bonus/expiration
- amount: 积分数量 (发放为正,消费为负)
- debitAccount: 复式记账借方账户
- creditAccount: 复式记账贷方账户
- referenceId: 唯一交易引用
- status: COMPLETED/FAILED
为什么使用复式记账?
积分系统采用复式记账原则,以实现最高的准确性和可审计性。这是一个关键的设计决策,提供了几个重要优势:
什么是复式记账?
每笔积分交易记录交易的两个方面:
- 借方账户(Debit Account) - 积分来自哪里
- 贷方账户(Credit Account) - 积分去往哪里
交易示例:
{
type: "purchase",
amount: 100,
debitAccount: "PAYMENT:stripe_payment_123", // 来源
creditAccount: "WALLET:user_123", // 目的地
}
为什么这样设计?
1. 财务准确性和可审计性
传统的单式记账系统可能会丢失积分轨迹:
❌ 单式记账:"用户获得 100 积分"
问题:它们从哪里来?无法追踪来源。
✅ 复式记账:"100 积分从 PAYMENT:xyz 转移到 WALLET:user_123"
清晰:资金轨迹完整且可追溯。
2. 防欺诈和异常检测
系统可以检测异常:
// 如果总借方 ≠ 总贷方,说明有问题
const totalDebits = sumAllDebitTransactions();
const totalCredits = sumAllCreditTransactions();
if (totalDebits !== totalCredits) {
alert("⚠️ 检测到账目不一致!");
}
3. 全面的财务报告
您可以生成如下报告:
- 本月购买的积分与消费的积分各是多少?
- 哪些服务消耗了最多的积分?
- 有多少收入与未使用的积分相关?
// 示例:追踪积分购买收入
SELECT SUM(amount) FROM credits_transaction
WHERE debitAccount LIKE 'PAYMENT:%'
AND createdAt >= '2024-01-01'
4. 合规要求
对于处理资金或将积分作为货币的企业:
- SOC 2 合规:所有交易的审计轨迹
- 财务审计:清晰的资金流文档
- 税务报告:准确的收入确认
账户结构
系统使用以下账户类型:
系统账户(积分来源):
SYSTEM:subscription-grant
- 月度订阅积分SYSTEM:registration-bonus
- 欢迎奖励PAYMENT:purchase_id
- 购买的积分SYSTEM:expiration
- 过期积分清理
用户账户(积分目的地):
WALLET:user_id
- 用户积分余额SERVICE:service_name
- 服务消费
交易流程示例:
购买:
PAYMENT:stripe_xyz_123 → WALLET:user_123 (100 积分)
消费:
WALLET:user_123 → SERVICE:google:chat (2 积分)
过期:
WALLET:user_123 → SYSTEM:expiration (50 积分)
相比简单余额追踪的优势
简单余额(❌):
UPDATE user_balance SET balance = balance + 100
-- 问题:
-- - 没有积分来源的历史记录
-- - 无法追踪差异
-- - 没有审计轨迹
-- - 容易出错
复式记账(✅):
-- 交易 1:记录转账
INSERT INTO credits_transaction (
debitAccount: 'PAYMENT:xyz',
creditAccount: 'WALLET:user_123',
amount: 100
)
-- 交易 2:更新批次和余额
-- 两者都被跟踪和验证
实际场景的优势
场景 1:用户纠纷
用户:"我购买了 500 积分,但只有 400!"
使用复式记账,您可以展示:
- 购买:PAYMENT:xyz → WALLET:user (500 积分) ✓
- 消费 1:WALLET:user → SERVICE:chat (50 积分) ✓
- 消费 2:WALLET:user → SERVICE:image (50 积分) ✓
- 总计:500 - 100 = 400 积分 ✓
场景 2:收入对账
CFO:"有多少资金被未使用的积分占用?"
查询:
SELECT SUM(amount) FROM credits_transaction
WHERE debitAccount LIKE 'PAYMENT:%'
AND userId IN (SELECT userId FROM credits_balance WHERE balance > 0)
答案:$12,450 的未兑换积分价值
场景 3:系统完整性检查
// 每日完整性检查
async function verifySystemIntegrity() {
// 汇总所有发放的积分
const issued = await db
.select({ total: sum(creditsTransaction.amount) })
.where(like(creditsTransaction.debitAccount, 'PAYMENT:%'));
// 汇总所有用户余额
const balances = await db
.select({ total: sum(creditsBalance.balance) });
// 汇总所有消费的积分
const consumed = await db
.select({ total: sum(creditsTransaction.amount) })
.where(like(creditsTransaction.creditAccount, 'SERVICE:%'));
// issued 应该等于 balances + consumed
if (issued !== balances + consumed) {
throw new Error("检测到账目不匹配!");
}
}
设计权衡
优点:
- ✅ 完整的审计轨迹
- ✅ 欺诈检测
- ✅ 财务准确性
- ✅ 合规性
- ✅ 报告便捷
缺点:
- ⚠️ 实现稍微复杂
- ⚠️ 更多数据库存储(值得!)
- ⚠️ 需要理解会计概念
为什么值得:
即使是小的积分差异也可能:
- 导致用户投诉和退款
- 使追踪错误变得不可能
- 造成税务和合规问题
- 损害用户信任
相比于带来的好处,增加的复杂性是最小的,特别是对于处理真实资金的生产系统。
💡 工作原理
1. 积分发放(注册奖励示例)
当用户注册时:
import { grantRegistrationBonus } from "@/credits/actions";
// 用户注册后自动调用
await grantRegistrationBonus(userId);
执行流程:
- 创建新的积分批次,包含 20 积分(可配置)
- 设置过期日期(默认 30 天)
- 记录类型为
registration_bonus
的交易 - 更新用户总余额
- 创建复式记账记录
2. 积分消费 (FIFO)
当服务使用积分时:
import { consumeCreditsForService } from "@/credits/actions";
// 为 AI 服务消费积分
const result = await consumeCreditsForService({
userId: "user_123",
service: "google:chat", // 消耗 2 积分(已配置)
});
// 或指定自定义数量
const result = await consumeCreditsForService({
userId: "user_123",
service: "custom_service",
amount: 5, // 自定义数量
description: "自定义 AI 模型使用",
});
FIFO 流程:
- 获取所有按过期日期排序的活动批次
- 从最早的批次开始消费
- 如果批次耗尽,移至下一批次
- 记录详细的消费日志
- 如果批次完全消耗,更新批次状态
- 创建交易记录
示例:
用户拥有:
- 批次 A: 10 积分 (5 天后过期)
- 批次 B: 50 积分 (25 天后过期)
服务需要 15 积分:
→ 从批次 A 消费 10 积分 (现已耗尽)
→ 从批次 B 消费 5 积分 (剩余 45)
3. 月度订阅积分
对于订阅计划用户:
// 由定时任务自动调用
import { processDailyGrant } from "@/credits/actions";
// 每天 01:00 UTC 运行(可配置)
const results = await processDailyGrant();
执行流程:
- 查找所有活动订阅
- 检查订阅创建周年日
- 验证本月尚未发放
- 发放月度积分(例如 Pro 计划 200 积分)
- 记录
grantPeriod
(YYYY-MM) - 防止重复发放
4. 积分过期
自动过期处理:
// 由定时任务自动调用
import { processExpiredCredits } from "@/credits/actions";
// 每天运行以处理过期批次
const result = await processExpiredCredits();
执行流程:
- 查找所有
expiresAt < now()
的批次 - 更新批次状态为
EXPIRED
- 创建过期交易
- 更新用户余额
- 维护准确的账目
⚙️ 配置说明
主配置 (src/config/index.ts
)
export const appConfig = {
credits: {
// 启用/禁用整个积分系统
enabled: true,
// 注册奖励设置
registration: {
enabled: true,
amount: 20, // 新用户免费积分
validityDays: 30, // 30 天后过期
},
// 系统限制
limitations: {
allowFreeUserPurchasePackages: true, // 免费用户可以购买积分
maxUserBalance: 10000, // 最大积分限制
},
// 订阅计划的月度积分
subscription: {
free: {
id: "free",
enabled: true,
monthlyGrant: 10, // 免费用户每月 10 积分
validityDays: 30,
},
pro: {
id: "pro",
enabled: true,
monthlyGrant: 200, // Pro 用户每月 200 积分
validityDays: 30,
},
},
// 一次性积分包
packages: {
lite: {
id: "lite",
name: "Lite",
credits: 100, // 基础积分
priceId: process.env.CREDIT_LITE_PRICE_ID, // Stripe/Creem 价格 ID
amount: 9.99, // 美元价格
currency: "USD",
validityDays: 90, // 3 个月
bonus: 10, // 奖励积分
},
standard: {
id: "standard",
name: "Standard",
credits: 500,
priceId: process.env.CREDIT_STANDARD_PRICE_ID,
amount: 29.99,
currency: "USD",
validityDays: 90,
bonus: 50,
},
pro: {
id: "pro",
name: "Pro",
credits: 1500,
priceId: process.env.CREDIT_PRO_PRICE_ID,
amount: 79.99,
currency: "USD",
popular: true, // 突出显示此套餐
validityDays: 90,
bonus: 200,
},
max: {
id: "max",
name: "Max",
credits: 5000,
priceId: process.env.CREDIT_MAX_PRICE_ID,
amount: 199.99,
currency: "USD",
validityDays: 365, // 1 年有效期
bonus: 1000,
},
},
// 不同服务的积分消耗率
consumption: {
"google:fast": 1, // 快速 AI 模型:1 积分
"google:chat": 2, // 聊天模型:2 积分
"google:reasoning": 4, // 高级推理:4 积分
"google:image": 5, // 图像生成:5 积分
},
},
};
设计理念
为什么使用 FIFO(先进先出)?
FIFO 确保公平性并鼓励及时使用积分:
- 对用户公平:最早的积分先被使用
- 鼓励参与:用户有动力在过期前使用积分
- 可预测性:用户了解哪些积分会先过期
- 收入优化:减少未使用积分的累积
为什么要有过期机制?
积分过期有多个目的:
- 防止囤积:鼓励积极使用
- 收入确认:有助于财务规划
- 用户参与:为平台使用创造紧迫感
- 公平变现:平衡用户价值和业务需求
配置的影响
不同的配置会产生不同的结果:
保守模式(较长过期时间,较低发放):
registration: { amount: 10, validityDays: 14 },
subscription: {
pro: { monthlyGrant: 100, validityDays: 30 }
}
- 用户购买更多积分包
- 付费计划转化率更高
- 使用模式更可预测
慷慨模式(较长过期时间,较高发放):
registration: { amount: 50, validityDays: 90 },
subscription: {
pro: { monthlyGrant: 500, validityDays: 60 }
}
- 更好的用户体验
- 更高的留存率
- 鼓励平台探索
- 潜在的病毒式增长
🚀 快速开始
步骤 1:配置环境变量
在 .env
文件中添加:
# ---------积分系统----------
# 首先在 Stripe/Creem 控制面板中创建这些产品
CREDIT_LITE_PRICE_ID=price_xxx # Lite 套餐价格 ID
CREDIT_STANDARD_PRICE_ID=price_xxx # Standard 套餐价格 ID
CREDIT_PRO_PRICE_ID=price_xxx # Pro 套餐价格 ID
CREDIT_MAX_PRICE_ID=price_xxx # Max 套餐价格 ID
# ---------定时任务----------
# 生成随机的 16 字符密钥
CRON_SECRET=your-secret-key-here
步骤 2:在支付提供商中创建价格
为每个积分包在 Stripe 或 Creem 中创建一次性支付产品:
Stripe:
- 进入 Stripe 控制面板 → Products
- 点击 "Add product"
- 设置定价为一次性支付
- 复制价格 ID(以
price_
开头) - 添加到环境变量
Creem:
- 进入 Creem 控制面板 → Products
- 创建一次性产品
- 复制产品 ID
- 添加到环境变量
步骤 3:设置定时任务
系统需要两个定时任务:
发放积分定时任务(每天)
URL: https://yourdomain.com/api/jobs/credits/grant
计划: 每天 01:00 UTC
请求头: Authorization: Bearer your-cron-secret
过期积分定时任务(每天)
URL: https://yourdomain.com/api/jobs/credits/expire
计划: 每天 04:00 UTC
请求头: Authorization: Bearer your-cron-secret
查看定时任务设置指南了解详细的平台特定说明。
📖 下一步
🤔 常见问题
可以禁用特定计划的积分吗?
可以!在订阅配置中设置 enabled: false
:
subscription: {
free: {
enabled: false, // 免费用户不获得月度积分
},
}
免费用户可以购买积分包吗?
由 allowFreeUserPurchasePackages
控制:
limitations: {
allowFreeUserPurchasePackages: true, // 允许
}
积分过期时会发生什么?
- 批次状态更改为
EXPIRED
- 从用户余额中扣除积分
- 创建审计交易记录
- 不通知用户(您可以实现通知功能)
如何追踪积分使用情况?
使用用户交易页面或直接查询:
import { getUserTransactions } from "@/credits/actions";
const { items } = await getUserTransactions({
userId: "user_123",
pageIndex: 0,
pageSize: 10,
});