用量与 Bonus 系统
Space 用量追踪、计划限制、Bonus 额度和兑换码
用量与 Bonus 系统
ProductReady 包含完整的用量追踪和 Bonus 系统:
- 追踪资源使用 - 按 Space 追踪 AI Credits、Posts、Storage
- 执行计划限制 - Free、Pro、Enterprise 不同配额
- 发放 Bonus 额度 - 通过兑换码或促销活动
- 展示用量 - Dashboard 进度条和警告提示
此系统与支付计费系统分离。它追踪用户消耗了什么,而 billing 处理如何付款。
架构概览
┌─────────────────────────────────────────────────────────────┐
│ 计划配置 │
│ (src/config/plan-limits.ts + pricing-plans.ts) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 数据库 Schema │
│ ┌──────────────┐ ┌─────────────────┐ ┌────────────────┐ │
│ │ spaceUsage │ │ spaceBonusUsage │ │ bonusTemplates │ │
│ │ (用量追踪) │ │ (Bonus 额度) │ │ (管理员配置) │ │
│ └──────────────┘ └─────────────────┘ └────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 服务层 │
│ ┌──────────────────┐ ┌─────────────────────────────────┐ │
│ │ usage-service.ts │ │ bonus-service.ts │ │
│ │ - getUsageSummary│ │ - grantBonusFromTemplate │ │
│ │ - consumeUsage │ │ - grantManualBonus │ │
│ └──────────────────┘ └─────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘计划配置
计划限制
在 src/config/plan-limits.ts 定义各计划的资源限制:
export type UsageType = "ai_credits" | "posts" | "storage";
export type PeriodType = "monthly" | "total";
export interface PlanLimits {
aiCredits: UsageLimit;
posts: UsageLimit;
storage: UsageLimit;
openApi: FeatureFlag;
}
const MB = 1024 * 1024;
const GB = MB * 1024;
export const UNLIMITED = -1;
export const PLAN_LIMITS: Record<PlanKey, PlanLimits> = {
Free: {
aiCredits: { limit: 50, period: "monthly", label: "AI Credits" },
posts: { limit: 100, period: "total", label: "Posts" },
storage: { limit: 100 * MB, period: "total", label: "Storage" },
openApi: { enabled: false, label: "OpenAPI Access" },
},
Pro: {
aiCredits: { limit: 500, period: "monthly", label: "AI Credits" },
posts: { limit: 1000, period: "total", label: "Posts" },
storage: { limit: 10 * GB, period: "total", label: "Storage" },
openApi: { enabled: true, label: "OpenAPI Access" },
},
Enterprise: {
aiCredits: { limit: UNLIMITED, period: "monthly", label: "AI Credits" },
posts: { limit: UNLIMITED, period: "total", label: "Posts" },
storage: { limit: UNLIMITED, period: "total", label: "Storage" },
openApi: { enabled: true, label: "OpenAPI Access" },
},
};数据库 Schema
Space 用量表
export const spaceUsage = pgTable("space_usage", {
spaceId: text("space_id").primaryKey(),
aiCreditsUsed: integer("ai_credits_used").default(0).notNull(),
postsUsed: integer("posts_used").default(0).notNull(),
storageUsed: bigint("storage_used", { mode: "number" }).default(0).notNull(),
currentPeriod: text("current_period").notNull(), // "YYYY-MM"
});Bonus 用量表
export const spaceBonusUsage = pgTable("space_bonus_usage", {
id: text("id").primaryKey(),
spaceId: text("space_id").notNull(),
type: text("type").notNull(), // ai_credits, posts, storage
amount: integer("amount").notNull(), // 总 Bonus 数量
used: integer("used").default(0), // 已消耗数量
source: text("source").notNull(), // redemption, promotion, manual
sourceName: text("source_name").notNull(),
expiresAt: timestamp("expires_at"), // null = 永不过期
});Bonus 模板表
export const spaceBonusTemplates = pgTable("space_bonus_templates", {
id: text("id").primaryKey(),
name: text("name").notNull(),
type: text("type").notNull(), // ai_credits, posts, storage
amount: integer("amount").notNull(),
durationDays: integer("duration_days"), // null = 永不过期
applicablePlans: text("applicable_plans").array(),
isActive: boolean("is_active").default(true).notNull(),
});用量服务
获取用量摘要
import { getUsageSummary } from "~/lib/billing";
const summary = await getUsageSummary(spaceId);
// 返回:
// {
// plan: "Pro",
// items: [
// { type: "ai_credits", used: 150, limit: 1100, percentage: 13.6, isWarning: false },
// ],
// }消耗用量
import { consumeUsage } from "~/lib/billing";
const result = await consumeUsage(spaceId, "ai_credits", 10);
// 返回: { success: true, remaining: 90 }
// 或: { success: false, error: "quota_exhausted" }消耗优先级
- Bonus 额度优先(最早过期的先用)
- 计划配额(Bonus 用完后)
- 拒绝(所有配额用完)
兑换码系统
兑换码支持多态奖励:
| 类型 | 描述 |
|---|---|
plan | 升级 Space 到指定计划 |
bonus | 从模板发放 Bonus 额度 |
创建兑换码(管理员)
await trpc.redemptionCodes.create.mutate({
code: "PROMO2024",
rewardType: "plan",
planType: "Pro",
durationDays: 30,
maxRedemptions: 100,
});await trpc.redemptionCodes.create.mutate({
code: "BONUS500",
rewardType: "bonus",
bonusTemplateId: "tmpl_500_credits",
durationDays: 60,
maxRedemptions: 50,
});兑换码(用户)
const result = await trpc.redemptionCodes.redeem.mutate({
code: "PROMO2024",
spaceId: "space_123",
});给最终用户的兑换指南
作为开发者,你可以使用以下模板来指导用户完成兑换流程。只需复制并自定义下方的消息即可。
当向用户分发兑换码时,你可以使用这些可复制粘贴的模板:
模板 1:计划升级码
您好 [用户名],
感谢您对升级工作空间的兴趣!这是您的专属兑换码:
🎁 兑换码:[您的兑换码]
兑换步骤,升级到 [Pro/Enterprise] 计划:
1. 访问:https://yourapp.com/redeem
2. 登录您的账号(新用户需要先注册)
3. 输入兑换码:[您的兑换码]
4. 选择要升级的工作空间
5. 点击"兑换码"按钮
您的工作空间将立即升级!此兑换码有效期[30天 / 至 年/月/日 / 永久]。
如有任何问题,欢迎联系我们的客服团队。
祝好,
[您的团队名称]模板 2:Bonus 额度码
您好 [用户名],
好消息!我们为您准备了 Bonus 额度,让您体验我们的高级功能。
🎁 Bonus 码:[您的兑换码]
领取步骤:
1. 访问 https://yourapp.com/redeem
2. 登录您的账号
3. 输入兑换码:[您的兑换码]
4. 选择您的工作空间
5. 点击"兑换码"
您将立即获得 [X] 个 AI Credits / Posts / 存储空间,可使用 [期限]。
尽情探索我们的平台吧!
[您的团队名称]模板 3:简洁邮件格式
主题:您的兑换码已就绪!🎁
您好 [用户名],
您的兑换码:[您的兑换码]
兑换地址:https://yourapp.com/redeem
此兑换码将[升级您的计划至 Pro / 授予您 X 个 Bonus 额度]。
有疑问?请回复此邮件。
谢谢,
[您的团队]兑换页面功能
用户在 /redeem 页面将看到友好的界面,包括:
- 自动识别兑换码:如果您分享链接
https://yourapp.com/redeem?code=PROMO2024,兑换码会自动填充 - 登录保护:用户必须登录后才能兑换
- 工作空间选择:用户可以选择将兑换码应用到哪个工作空间
- 即时确认:显示成功消息和升级/Bonus 详情
- 当前计划可见性:用户在兑换前可以看到当前计划
分发最佳实践
- 使用直链:分享
https://yourapp.com/redeem?code=您的兑换码来预填兑换码 - 设定预期:告诉用户他们将获得什么(计划升级、额度数量、有效期)
- 提供支持:附上客服联系方式以解决兑换问题
- 追踪兑换:在管理面板
/systemadmin/redemption-codes监控兑换率 - 设置过期时间:使用限时兑换码营造紧迫感
UI 组件
用量面板
import { UsagePanel } from "~/components/dashboard/usage-panel";
<UsagePanel spaceId={spaceId} />Bonus 表格
import { BonusTable } from "~/components/dashboard/bonus-table";
<BonusTable spaceId={spaceId} />管理员页面
Bonus 模板管理
路径: /systemadmin/bonus-templates
功能:
- 创建/编辑/删除 Bonus 模板
- 设置数量、有效期、适用计划
- 切换启用状态
兑换码管理
路径: /systemadmin/redemption-codes
功能:
- 创建单个或批量兑换码
- 选择奖励类型(计划或 Bonus)
- 查看兑换历史
- 导出 CSV
最佳实践
1. 操作前检查用量
async function generateContent(spaceId: string) {
const result = await consumeUsage(spaceId, "ai_credits", 1);
if (!result.success) {
throw new Error("AI 额度已用完,请升级计划。");
}
// 继续生成...
}2. 80% 时显示警告
{item.isWarning && (
<Alert variant="warning">
您已使用 {item.percentage}% 的 {item.label}。
考虑升级您的计划。
</Alert>
)}3. 通过 Bonus 激励用户
// 用户完成引导后发放 Bonus
await grantPromotionalBonus(
spaceId,
"ai_credits",
50,
"完成引导奖励",
30 // 30 天有效期
);