关于 ship-credits
ship-credits 是用于为现有应用搭建积分系统的编码技能。它先检查项目,再根据技术栈和用户选择配置数据库、后端、支付、前端状态与界面。
主要功能
- 01检查项目技术栈
- 02积分数据结构与交易记录
- 03余额校验和扣除中间件
- 04支付 Webhook 集成
- 05前端状态与积分界面
- 06优惠码和管理工具
适用场景
- 01为现有应用添加付费积分
- 02记录并审计积分变动
- 03余额不足时阻止相关操作
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ tushaarmehtaa/ tushar-skills --skill ship-credits安装后在 Claude Code 中输入/ship-credits确认已加载。
附带文件references/database-schemas.mdreferences/payment-providers.md
详细介绍
搭建完整的积分系统——包括数据库架构、后端中间件、支付 Webhook、前端状态和 UI 组件。先读取项目,再基于现有内容进行构建。
阶段 1:了解项目
编写任何代码之前,先阅读代码库并回答以下问题:
1.1 检测技术栈
- 后端:Next.js API 路由 / FastAPI / Express / Django / Rails?
- 数据库:Supabase / Postgres / PlanetScale / MongoDB / Prisma?
- 身份验证:Clerk / NextAuth / Supabase Auth / Firebase Auth / 自定义 JWT?
- 前端状态:Zustand / Redux / React Context / Jotai / 原生实现?
- 是否已有支付集成?:检查是否导入了 Stripe / Lemon Squeezy / Dodo / Paddle
1.2 询问用户
搭建之前,先确认以下决策:
I'll set up credits for your [framework] app with [database].
Quick decisions needed:
1. What costs credits? (e.g., "AI generation = 5, image gen = 4, export = 2")
2. Free tier: How many credits on signup? (e.g., 50)
3. Payment provider preference? (Stripe / Lemon Squeezy / Dodo / manual only)
4. Credit pack pricing? (e.g., "$5 = 100 credits, $10 = 250 credits")
Defaults: 50 free credits, per-action costs you define, Stripe.
阶段 2:数据库架构
阅读 references/database-schemas.md,然后只实现与检测到的数据库匹配的变体。
每个变体都必须提供:
- 用户记录中的非负余额
- 不可变的积分交易审计记录
- 支持按用户和时间查询交易的索引
- 不会因并发竞争而导致余额低于零的原子余额变更
- 仅在用户提出要求时提供可选的邀请码存储
交易账本就绪后告知用户,并说明已修改的迁移文件或架构文件。
阶段 3:后端积分服务
创建积分服务模块,并实现以下核心函数。根据项目的语言和框架进行适配。
核心函数
get_credits(user_identifier) — 获取当前余额:
Input: auth_id or email (whatever the project uses to identify users)
Output: integer (credit balance) or null (user not found)
Logic: Query users table, return credits column
deduct_credits(user_identifier, amount, reason) — 消耗积分:
Input: user identifier, amount to deduct, reason string
Output: boolean (success/failure)
Logic:
1. Get current credits
2. If credits < amount → return false (DO NOT go negative)
3. Update users.credits = credits - amount
4. Insert credit_transactions record with negative amount
5. Return true
add_credits(user_identifier, amount, reason, metadata) — 添加积分:
Input: user identifier, amount to add, reason, optional metadata (payment_id, promo_code, etc.)
Output: new balance
Logic:
1. Update users.credits = credits + amount
2. Insert credit_transactions record with positive amount
3. Return new balance
重要实现细节:
- 扣除函数必须先检查余额,再扣除积分——绝不能信任前端
- 使用数据库级约束或事务来防止竞态条件
- 对于高吞吐场景:使用
UPDATE users SET credits = credits - $amount WHERE id = $id AND credits >= $amount并检查受影响的行数 - 始终将记录写入 credit_transactions——这是财务审计记录
积分检查中间件
创建在任何消耗积分的端点之前运行的中间件:
适用于 Next.js API 路由:
// middleware pattern — adapt to project's auth
export function withCredits(handler, cost) {
return async (req, res) => {
const user = await getAuthUser(req); // from project's auth
const credits = await getCredits(user.id);
if (credits < cost) {
return res.status(402).json({
error: 'Insufficient credits',
required: cost,
balance: credits,
});
}
// Attach to request for handler to use
req.creditsCost = cost;
req.deductCredits = () => deductCredits(user.id, cost, 'api_action');
return handler(req, res);
};
}
适用于 FastAPI:
async def require_credits(amount: int):
async def dependency(request: Request):
user = await get_current_user(request) # from project's auth
credits = get_credits(user.id)
if credits is None or credits < amount:
raise HTTPException(
status_code=402,
detail={"error": "Insufficient credits", "required": amount, "balance": credits or 0}
)
return user
return Depends(dependency)
适用于 Express:
function requireCredits(cost) {
return async (req, res, next) => {
const credits = await getCredits(req.user.id);
if (credits < cost) {
return res.status(402).json({ error: 'Insufficient credits', required: cost, balance: credits });
}
req.creditsCost = cost;
next();
};
}
积分不足时应使用 HTTP 402 状态码。它的字面意思是“需要付款”。在错误处理器中明确处理该状态码——不要让它被通用的 4xx/5xx 处理器捕获。
积分费用配置
为各项费用创建唯一可信来源:
// config/credits.ts (or equivalent)
export const CREDIT_COSTS = {
// Define based on user's answer from Phase 1
GENERATE: 5,
REGENERATE: 1,
IMAGE: 4,
EXPORT: 2,
} as const;
export const FREE_CREDITS = 50;
export const CREDIT_PACKS = [
{ credits: 100, price_cents: 500, label: '$5' },
{ credits: 250, price_cents: 1000, label: '$10' },
{ credits: 600, price_cents: 2000, label: '$20' },
] as const;
阶段 4:支付集成
阅读 references/payment-providers.md,然后只实现阶段 1 中选定的支付服务商。如果项目已有支付服务商,不要再安装第二个。
每种支付服务商的实现都必须:
- 将用户标识符和积分数量写入结账元数据。
- 根据原始请求正文验证 Webhook 签名。
- 使用服务商的支付 ID 或事件 ID 确保幂等性。
- 仅在付款验证通过后添加积分并写入交易账本。
- 记录处理失败,并返回参考文档中说明的服务商安全确认响应。
阶段 5:前端积分状态
Zustand Store(React 项目推荐)
import { create } from 'zustand';
interface CreditsStore {
credits: number | null; // null = not loaded yet
setCredits: (credits: number | null) => void;
deduct: (amount: number) => void;
}
export const useCreditsStore = create<CreditsStore>((set) => ({
credits: null,
setCredits: (credits) => set({ credits }),
deduct: (amount) =>
set((state) => ({
credits: state.credits !== null ? Math.max(0, state.credits - amount) : null,
})),
}));
积分同步 Hook
// hooks/useCredits.ts
export function useCredits() {
const { credits, setCredits, deduct } = useCreditsStore();
// Fetch on mount (after auth)
const fetchCredits = async () => {
const res = await fetch('/api/user/credits');
const data = await res.json();
setCredits(data.credits);
};
// Optimistic deduction — update UI immediately, backend confirms async
const spendCredits = async (amount: number, action: () => Promise<void>) => {
if (credits !== null && credits < amount) {
// Trigger low credit UI
return { success: false, reason: 'insufficient' };
}
deduct(amount); // Optimistic UI update
try {
await action(); // The actual API call (which also deducts server-side)
return { success: true };
} catch (error) {
// Refund the optimistic deduction
await fetchCredits();
return { success: false, reason: 'error' };
}
};
const hasEnough = (amount: number) => credits !== null && credits >= amount;
return { credits, fetchCredits, spendCredits, hasEnough };
}
关键 UX 模式
在页眉或导航栏中显示积分:
- 在用户头像或菜单附近显示当前余额
- 格式:“42 积分”或只显示“42”,并配上硬币或代币图标
- 如果值为 null(正在加载),显示骨架屏或不显示任何内容——加载时绝不能显示 0
低积分警告阈值:
const LOW_CREDIT_THRESHOLD = 20; // Show gentle reminder
const CRITICAL_THRESHOLD = 10; // Show prominent warning
const ZERO_THRESHOLD = 0; // Block action, show buy modal
操作前检查积分: 执行任何消耗积分的操作之前,先在客户端检查:
if (!hasEnough(CREDIT_COSTS.GENERATE)) {
openBuyCreditsModal();
return;
}
这样可以避免不必要的 API 调用。服务器仍会进行验证——这只是改善 UX。
阶段 6:UI 组件
创建以下组件,并与项目现有的设计系统保持一致。
1. 额度余额显示
- 显示在页眉/导航栏中
- 操作后实时更新
- 额度变化时播放轻微的脉冲动画
- 点击后打开购买模态框
2. 购买额度模态框/滑块
- 显示额度包选项及价格
- 突出显示性价比最高的额度包
- 显示额度可购买的内容:“100 个额度 ≈ 20 次生成 + 5 张图片”
- CTA 按钮会创建结账流程并重定向
3. 额度不足警告
- 余额低于阈值时显示
- 额度较低时不阻塞操作(横幅/提示)
- 额度为零时阻塞操作(带有购买 CTA 的模态框)
- 显示额度消耗,作为提醒
- 主要 CTA 为“购买额度”;如果额度 > 0,则提供“我会谨慎使用”关闭选项
4. 额度消耗提示
- 在每个操作按钮旁显示消耗:“生成(5 个额度)”
- 额度不足时将按钮置灰
- 在禁用的按钮上显示工具提示:“你还需要 X 个额度”
5. 结账成功页面
- “/checkout/success” 路由
- 轮询 /api/user/credits,直到余额更新(Webhook 可能需要几秒钟)
- 显示彩纸/庆祝动画和新余额
- 提供返回主应用的 CTA
6. 交易记录(可选,但建议添加)
- 列出所有额度变动的表格/列表
- 列:日期、操作、金额(+/-)、变动后余额
- 按原因筛选(购买、消费、奖励)
- 有助于建立用户信任——用户可以清楚地看到额度的去向
第 7 阶段:促销码系统
兑换端点
POST /api/credits/redeem
Body: { code: string }
Logic:
1. Get authenticated user
2. Find promo code by code string
3. Validate:
- Code exists
- Not expired (expires_at is null or > now)
- times_used < max_uses
- If email is set on code, must match user's email
4. Add credits to user
5. Increment times_used on promo code
6. Log transaction with reason='promo_code', metadata={ code }
7. Return { credits_added, new_balance }
管理员创建促销码端点
POST /api/admin/promo-codes
Body: { credits_amount, max_uses?, email?, expires_in_days?, custom_code? }
Auth: Admin only
Logic:
1. Generate random 8-char code if no custom_code
2. Insert into promo_codes table
3. Return { code, credits_amount, expires_at }
促销码对于以下用途必不可少:
- Beta 用户引导
- 网红/合作伙伴推广
- 客户支持(“很抱歉给您带来问题,这里有 50 个额度”)
- 营销活动
第 8 阶段:管理工具
添加额度端点
POST /api/admin/add-credits
Body: { email: string, amount: number, reason?: string }
Auth: Admin only (check admin secret header or admin role)
Logic:
1. Find user by email
2. Add credits
3. Log transaction with reason (default: 'admin_grant')
4. Return { email, credits_added, new_balance }
额度分析(从 credit_transactions 读取)
适合在管理面板中展示的查询:
- 购买额度总数(reason='purchase' 的总和)
- 已消费额度总数(amount < 0 的总和)
- 未使用额度总数(所有用户的 credits 之和)
- 收入(购买次数 * 价格)
- 最活跃用户(按 user_id 分组,汇总消费额度)
- 按操作类型统计消费情况(按 reason 分组)
第 9 阶段:串联所有部分
创建好所有部分后,将它们连接起来:
- 身份验证同步:用户登录时,获取其额度余额并填充前端存储
- 每个受保护的端点:添加额度检查中间件,并设置相应消耗
- 每个消耗额度的 UI 操作:添加客户端 hasEnough 检查和服务端中间件
- 每次操作后:在前端乐观扣除额度,并通过 API 响应确认
- Webhook 路由:向支付服务提供商注册,并使用 CLI 工具测试
- 成功页面:创建 /checkout/success,并实现轮询逻辑
验证清单
完成脚手架搭建后,逐一检查以下流程:
Flow 1: New User Signup
[ ] User signs up → gets FREE_CREDITS
[ ] Credit balance shows in UI
[ ] Transaction logged: reason='signup_bonus'
Flow 2: Spend Credits
[ ] User triggers action → credits deducted
[ ] UI updates optimistically
[ ] Server validates balance before processing
[ ] 402 returned if insufficient
[ ] Transaction logged with correct reason
Flow 3: Buy Credits
[ ] User clicks buy → redirected to checkout
[ ] Payment succeeds → webhook received
[ ] Webhook verified → credits added
[ ] Idempotent (double webhook doesn't double-credit)
[ ] UI refreshes with new balance
[ ] Transaction logged: reason='purchase'
Flow 4: Promo Code
[ ] User enters code → credits added
[ ] Code usage incremented
[ ] Expired/used codes rejected
[ ] Email-restricted codes enforced
Flow 5: Edge Cases
[ ] Insufficient credits → clear error, buy CTA shown
[ ] Concurrent requests → no negative balance (DB constraint or check)
[ ] Webhook arrives before redirect → still works
[ ] Network error during action → optimistic deduction rolled back
告知用户哪些流程已接通,哪些需要手动测试。
重要说明
- 绝不要信任前端余额。 始终在服务端进行验证。前端余额仅用于提升用户体验。
- 记录每次额度变动。 credit_transactions 表才是事实来源,而不是 credits 列。如果两者不一致,以交易记录为准。
- Webhook 的幂等性不可妥协。 支付服务提供商会重试。重复发放额度会造成资金损失。
- HTTP 402 是正确的状态码。不是 403,也不是 400。402 表示“需要付款”。
- 从简单开始。 先提供一种价格的额度包。等有数据了解用户实际购买内容后,再添加不同档位。