mcpField
S
ship-credits

ship-credits

github.com/tushaarmehtaa开发1271 次安装2 周前更新
代码仓库

关于 ship-credits

ship-credits 是用于为现有应用搭建积分系统的编码技能。它先检查项目,再根据技术栈和用户选择配置数据库、后端、支付、前端状态与界面。

主要功能

  1. 01检查项目技术栈
  2. 02积分数据结构与交易记录
  3. 03余额校验和扣除中间件
  4. 04支付 Webhook 集成
  5. 05前端状态与积分界面
  6. 06优惠码和管理工具

适用场景

  1. 01为现有应用添加付费积分
  2. 02记录并审计积分变动
  3. 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 中选定的支付服务商。如果项目已有支付服务商,不要再安装第二个。

每种支付服务商的实现都必须:

  1. 将用户标识符和积分数量写入结账元数据。
  2. 根据原始请求正文验证 Webhook 签名。
  3. 使用服务商的支付 ID 或事件 ID 确保幂等性。
  4. 仅在付款验证通过后添加积分并写入交易账本。
  5. 记录处理失败,并返回参考文档中说明的服务商安全确认响应。

阶段 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 阶段:串联所有部分

创建好所有部分后,将它们连接起来:

  1. 身份验证同步:用户登录时,获取其额度余额并填充前端存储
  2. 每个受保护的端点:添加额度检查中间件,并设置相应消耗
  3. 每个消耗额度的 UI 操作:添加客户端 hasEnough 检查和服务端中间件
  4. 每次操作后:在前端乐观扣除额度,并通过 API 响应确认
  5. Webhook 路由:向支付服务提供商注册,并使用 CLI 工具测试
  6. 成功页面:创建 /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 表示“需要付款”。
  • 从简单开始。 先提供一种价格的额度包。等有数据了解用户实际购买内容后,再添加不同档位。

评论

支持 Markdown