logoPay4SaaS
消费管理

访问控制

访问控制解决两个问题:这个用户能不能用我的产品? 以及 该扣多少?

Pay4SaaS 提供两个维度的访问控制:

  • 基础准入allowed)—— 能/不能,适用全部四种模式
  • 等级权限useTierAccess)—— 判断用户的订阅等级够不够用某个功能,仅订阅模式适用

首页 Hero 区(components/landing/Hero.tsx)有完整的前端调用示例,包括权限判断、消费积分、赠送积分三种场景。

四种模式速览

在看 API 之前,先搞清楚你的产品用的是哪种模式——它决定了 alloweduseService 的行为:

模式allowed 的含义useService 的行为
积分模式购买过积分(purchased_credits > 0SERVICE_COSTS[serviceType] 扣积分
无限订阅有活跃订阅(含试用期)只记录用量,不扣积分;isUnlimited = true
配额订阅有活跃订阅按优先级扣减:配额 → 赠送积分 → 购买积分;配额每月重置
买断制有买断记录只记录用量,不扣积分;isUnlimited = true

useAccess Hook

import { useAccess } from '@/hooks/useAccess'

function MyComponent() {
  const { status, loading, error, refreshStatus, useService } = useAccess()

  if (loading) return <div>加载中...</div>

  if (!status.allowed) {
    return <div>请先订阅或购买积分</div>
  }

  return <div>欢迎使用!</div>
}

后端等价写法:

import { checkUserAccess } from '@/lib/payment/access'

const access = await checkUserAccess(userId)

if (!access.allowed) {
  return Response.json({ error: 'No access' }, { status: 403 })
}

status 对象

{
  allowed: boolean,          // 是否有权限
  accessType: AccessType,    // 权限类型(见下表)
  details: {
    hasSubscription: boolean,
    subscriptionPlan: string,        // basic / pro / max
    subscriptionStatus: string,
    subscriptionBillingCycle: string, // monthly / yearly
    subscriptionEndDate: string,
    subscriptionProvider: string,
    hasLifetime: boolean,
    lifetimePlan: string,
    availableCredits: number,
    quota: {                         // 仅配额订阅模式
      monthlyLimit: number,
      used: number,
      remaining: number,
      resetDate: string
    },
    isUnlimited: boolean,
    hasUsedTrial: boolean,
  }
}

accessType 值

说明
subscription_unlimited无限订阅
subscription_quota配额订阅
lifetime买断
credits积分
none无权限

useService —— 消费一次服务

用户点击「生成」「使用」等按钮时,调用 useService 一步完成权限判断和扣费:

const { useService } = useAccess()

async function handleGenerate() {
  const result = await useService(
    'article_generation',    // 服务类型(必须是 SERVICE_COSTS 中的 key)
    '生成了一篇博客文章',       // 描述(可选)
    'article-123'            // 关联 ID(可选)
  )

  if (result.success) {
    console.log('剩余积分:', result.remainingCredits)
  } else {
    console.log('错误:', result.message)
  }
}

重要: 前端只传 serviceType,实际扣减金额由后端 config/payment.ts 中的 SERVICE_COSTS 决定,用户无法通过 DevTools 篡改。

后端等价写法:

import { fastConsumeService } from '@/lib/payment/access'
import { SERVICE_COSTS } from '@/config/payment'

const amount = SERVICE_COSTS['article_generation']
const result = await fastConsumeService(
  userId,
  amount,               // 从 SERVICE_COSTS 查表获取
  'article_generation', // 服务类型
  '生成文章',            // 描述(可选)
  relatedId             // 关联 ID(可选)
)

if (!result.success) {
  return Response.json({ error: result.message }, { status: 403 })
}

fastConsumeService 通过数据库 RPC 原子扣减积分/配额。扣减金额始终来自服务端 SERVICE_COSTS 配置。

SERVICE_COSTS —— 定价表

所有服务的消费金额统一配置:

// config/payment.ts
export const SERVICE_COSTS: Record<string, number> = {
  'demo-consume': 25,
  'article_generation': 10,  // 你的自定义服务
}

添加新服务只需加一行,无需其他改动。

useService 返回值

{
  success: boolean,
  accessType: AccessType,     // 实际使用的权限类型
  remainingCredits: number,
  message: string,
  error?: string
}

等级权限 —— 按订阅等级解锁功能

等级权限解决的是另一个问题:不关心「用户有没有积分」,只问 「用户的订阅等级够不够用这个功能?」 它不扣积分、不记录用量,纯粹是一道门。

  • 等级关系:basic < pro < max(由 SUBSCRIPTION_PLANS 声明顺序决定)
  • 仅适用于订阅模式——买断和积分没有等级概念
  • 没有订阅的用户永远通不过等级判断

useTierAccess —— 前端

import { useTierAccess } from '@/hooks/useTierAccess'

function ProFeatureButton() {
  const canUse = useTierAccess('pro')

  if (!canUse) {
    return (
      <button onClick={() => router.push('/pricing?plan=pro')}>
        升级到 Pro 解锁
      </button>
    )
  }

  return <button onClick={handleDoFeature}>使用 Pro 功能</button>
}

hasTierAccess —— 后端

后端必须再验证一次——前端 disabled 只是 UX,用户可以绕过。

import { hasTierAccess } from '@/lib/payment/access'

export async function POST(req) {
  const userId = await getUser(req)

  if (!await hasTierAccess(userId, 'pro')) {
    return Response.json({ error: 'tier_required', required: 'pro' }, { status: 403 })
  }

  return doProFeature()
}

allowed vs useTierAccess —— 什么时候用哪个

status.alloweduseTierAccess('pro')
问的问题「有没有?」(积分、订阅、或买断)「等级够不够用这个功能?」
适用模式全部四种仅订阅模式

实践建议:

  • 积分模式status.allowed + useService
  • 订阅模式status.allowed 判断通用功能,useTierAccess 判断高级功能
  • 混合场景 → 两者配合:allowed 判断基础准入,useTierAccess 判断高级功能

文档首页

回到完整实施目录。

价格方案

查看订阅、积分和终身买断方案。

博客

阅读更多 SaaS 支付和增长经验。

On this page