目录
系统概述
核心功能
- 租户管理 - 租户注册、配置、隔离
- 订阅管理 - 套餐、订阅、升级降级
- 计费系统 - 账单生成、支付、发票
- 用量统计 - API 调用、存储、带宽统计
- 多租户隔离 - 数据隔离、资源隔离
技术特点
- 数据库级租户隔离
- 基于租户的中间件
- 订阅状态管理
- 用量计量和限流
- 自动计费
完整目录树
app/
├── controller/
│ ├── api/
│ │ └── v1/
│ │ ├── TenantController.php # 租户管理
│ │ ├── SubscriptionController.php # 订阅管理
│ │ ├── BillingController.php # 计费管理
│ │ ├── UsageController.php # 用量查询
│ │ └── InvoiceController.php # 发票管理
│ └── webhook/
│ └── PaymentWebhookController.php # 支付回调
│
├── model/
│ └── eloquent/
│ ├── Tenant.php # 租户模型
│ ├── Subscription.php # 订阅模型
│ ├── Plan.php # 套餐模型
│ ├── Invoice.php # 发票模型
│ ├── Usage.php # 用量记录模型
│ └── TenantUser.php # 租户用户模型
│
├── service/
│ ├── tenant/
│ │ ├── CreateTenantService.php # 创建租户
│ │ ├── ProvisionTenantService.php # 租户初始化
│ │ └── SuspendTenantService.php # 暂停租户
│ ├── subscription/
│ │ ├── CreateSubscriptionService.php # 创建订阅
│ │ ├── UpgradeSubscriptionService.php # 升级订阅
│ │ ├── CancelSubscriptionService.php # 取消订阅
│ │ └── RenewSubscriptionService.php # 续费订阅
│ ├── billing/
│ │ ├── GenerateInvoiceService.php # 生成账单
│ │ ├── ProcessPaymentService.php # 处理支付
│ │ └── CalculateUsageService.php # 计算用量
│ └── usage/
│ ├── TrackUsageService.php # 记录用量
│ └── CheckQuotaService.php # 检查配额
│
├── domain/
│ ├── tenant/
│ │ ├── entity/
│ │ │ └── Tenant.php # 租户实体
│ │ ├── enum/ # 枚举
│ │ │ └── TenantStatus.php # 租户状态枚举
│ │ ├── vo/ # 值对象
│ │ │ ├── TenantSlug.php # 租户标识
│ │ │ └── TenantConfig.php # 租户配置
│ │ ├── event/
│ │ │ ├── TenantCreated.php
│ │ │ ├── TenantSuspended.php
│ │ │ └── TenantActivated.php
│ │ └── rule/
│ │ └── TenantProvisioningRule.php # 租户初始化规则
│ │
│ ├── subscription/
│ │ ├── entity/
│ │ │ ├── Subscription.php # 订阅实体
│ │ │ └── Plan.php # 套餐实体
│ │ ├── enum/ # 枚举
│ │ │ └── SubscriptionStatus.php # 订阅状态枚举
│ │ ├── vo/ # 值对象
│ │ │ ├── BillingCycle.php # 计费周期
│ │ │ └── PlanFeatures.php # 套餐功能
│ │ ├── event/
│ │ │ ├── SubscriptionCreated.php
│ │ │ ├── SubscriptionUpgraded.php
│ │ │ ├── SubscriptionCancelled.php
│ │ │ └── SubscriptionExpired.php
│ │ └── rule/
│ │ ├── UpgradeRule.php # 升级规则
│ │ └── CancellationRule.php # 取消规则
│ │
│ ├── billing/
│ │ ├── entity/
│ │ │ └── Invoice.php # 发票实体
│ │ ├── enum/ # 枚举
│ │ │ └── InvoiceStatus.php # 发票状态枚举
│ │ ├── vo/ # 值对象
│ │ │ ├── InvoiceNumber.php # 发票号
│ │ │ └── BillingAmount.php # 账单金额
│ │ └── rule/
│ │ └── InvoiceGenerationRule.php # 账单生成规则
│ │
│ └── usage/
│ ├── entity/
│ │ └── Usage.php # 用量实体
│ ├── enum/ # 枚举
│ │ └── UsageType.php # 用量类型枚举
│ ├── vo/ # 值对象
│ │ ├── Quota.php # 配额
│ │ └── UsageMetric.php # 用量指标
│ └── rule/
│ └── QuotaEnforcementRule.php # 配额限制规则
│
├── contract/
│ ├── repository/
│ │ ├── TenantRepositoryInterface.php
│ │ ├── SubscriptionRepositoryInterface.php
│ │ ├── InvoiceRepositoryInterface.php
│ │ └── UsageRepositoryInterface.php
│ ├── gateway/
│ │ └── PaymentGatewayInterface.php
│ └── service/
│ └── TenantResolverInterface.php # 租户解析接口
│
├── infrastructure/
│ ├── repository/
│ │ └── eloquent/
│ │ ├── EloquentTenantRepository.php
│ │ ├── EloquentSubscriptionRepository.php
│ │ ├── EloquentInvoiceRepository.php
│ │ └── EloquentUsageRepository.php
│ ├── gateway/
│ │ └── payment/
│ │ └── StripePaymentGateway.php
│ └── service/
│ ├── SubdomainTenantResolver.php # 子域名租户解析
│ └── HeaderTenantResolver.php # Header 租户解析
│
├── middleware/
│ ├── tenant/
│ │ ├── IdentifyTenantMiddleware.php # 识别租户
│ │ └── EnforceTenantScopeMiddleware.php # 强制租户隔离
│ ├── subscription/
│ │ └── CheckSubscriptionMiddleware.php # 检查订阅状态
│ └── usage/
│ └── TrackUsageMiddleware.php # 记录用量
│
├── process/
│ ├── task/
│ │ ├── GenerateInvoicesTask.php # 生成账单任务
│ │ ├── ProcessExpiredSubscriptionsTask.php # 处理过期订阅
│ │ └── AggregateUsageTask.php # 聚合用量统计
│ └── queue/
│ └── TenantProvisioningConsumer.php # 租户初始化消费者
│
└── support/
├── exception/
│ ├── TenantNotFoundException.php
│ ├── SubscriptionExpiredException.php
│ └── QuotaExceededException.php
└── trait/
└── BelongsToTenant.php # 租户关联 Trait模块划分
1. 租户管理模块
功能: 租户注册、配置、状态管理、数据隔离
核心类:
domain/tenant/entity/Tenant.php- 租户实体service/tenant/CreateTenantService.php- 创建租户middleware/tenant/IdentifyTenantMiddleware.php- 租户识别
2. 订阅管理模块
功能: 套餐管理、订阅创建、升级降级、续费取消
核心类:
domain/subscription/entity/Subscription.php- 订阅实体service/subscription/UpgradeSubscriptionService.php- 升级服务
3. 计费系统模块
功能: 账单生成、支付处理、发票管理
核心类:
domain/billing/entity/Invoice.php- 发票实体service/billing/GenerateInvoiceService.php- 生成账单
4. 用量统计模块
功能: API 调用统计、存储统计、配额限制
核心类:
domain/usage/entity/Usage.php- 用量实体service/usage/CheckQuotaService.php- 配额检查
关键代码示例
1. 租户实体
php
<?php
declare(strict_types=1);
namespace app\domain\tenant\entity;
use app\domain\tenant\enum\TenantStatus;
use app\domain\tenant\vo\TenantSlug;
use app\domain\tenant\event\TenantCreated;
final class Tenant
{
private array $domainEvents = [];
private function __construct(
private readonly int $id,
private readonly TenantSlug $slug,
private string $name,
private TenantStatus $status,
private array $config,
private readonly \DateTimeImmutable $createdAt
) {
}
public static function create(TenantSlug $slug, string $name): self
{
$tenant = new self(
id: 0,
slug: $slug,
name: $name,
status: TenantStatus::Active,
config: [],
createdAt: new \DateTimeImmutable()
);
$tenant->recordEvent(new TenantCreated($tenant));
return $tenant;
}
public function suspend(): void
{
$this->status = TenantStatus::Suspended;
}
public function activate(): void
{
$this->status = TenantStatus::Active;
}
public function id(): int
{
return $this->id;
}
public function slug(): TenantSlug
{
return $this->slug;
}
public function status(): TenantStatus
{
return $this->status;
}
private function recordEvent(object $event): void
{
$this->domainEvents[] = $event;
}
public function releaseEvents(): array
{
$events = $this->domainEvents;
$this->domainEvents = [];
return $events;
}
}2. 订阅实体
php
<?php
declare(strict_types=1);
namespace app\domain\subscription\entity;
use app\domain\subscription\enum\SubscriptionStatus;
use app\domain\subscription\vo\BillingCycle;
use app\domain\subscription\event\SubscriptionUpgraded;
final class Subscription
{
private array $domainEvents = [];
private function __construct(
private readonly int $id,
private readonly int $tenantId,
private int $planId,
private SubscriptionStatus $status,
private BillingCycle $billingCycle,
private \DateTimeImmutable $startDate,
private \DateTimeImmutable $endDate,
private readonly \DateTimeImmutable $createdAt
) {
}
public static function create(
int $tenantId,
int $planId,
BillingCycle $billingCycle
): self {
$startDate = new \DateTimeImmutable();
$endDate = $billingCycle->calculateEndDate($startDate);
return new self(
id: 0,
tenantId: $tenantId,
planId: $planId,
status: SubscriptionStatus::Active,
billingCycle: $billingCycle,
startDate: $startDate,
endDate: $endDate,
createdAt: new \DateTimeImmutable()
);
}
public function upgrade(int $newPlanId): void
{
$oldPlanId = $this->planId;
$this->planId = $newPlanId;
$this->recordEvent(new SubscriptionUpgraded($this, $oldPlanId, $newPlanId));
}
public function cancel(): void
{
$this->status = SubscriptionStatus::Cancelled;
}
public function isActive(): bool
{
return $this->status->isActive() && $this->endDate > new \DateTimeImmutable();
}
public function tenantId(): int
{
return $this->tenantId;
}
public function planId(): int
{
return $this->planId;
}
private function recordEvent(object $event): void
{
$this->domainEvents[] = $event;
}
public function releaseEvents(): array
{
$events = $this->domainEvents;
$this->domainEvents = [];
return $events;
}
}3. 租户识别中间件
php
<?php
declare(strict_types=1);
namespace app\middleware\tenant;
use app\contract\service\TenantResolverInterface;
use app\contract\repository\TenantRepositoryInterface;
use app\support\exception\TenantNotFoundException;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
final class IdentifyTenantMiddleware implements MiddlewareInterface
{
public function __construct(
private readonly TenantResolverInterface $tenantResolver,
private readonly TenantRepositoryInterface $tenantRepository
) {
}
public function process(Request $request, callable $next): Response
{
// 1. 解析租户标识(从子域名或 Header)
$tenantSlug = $this->tenantResolver->resolve($request);
if ($tenantSlug === null) {
throw new TenantNotFoundException('Tenant not identified');
}
// 2. 获取租户
$tenant = $this->tenantRepository->findBySlug($tenantSlug);
if ($tenant === null) {
throw new TenantNotFoundException("Tenant not found: {$tenantSlug}");
}
// 3. 检查租户状态
if (!$tenant->status()->isActive()) {
return json([
'error' => 'Tenant is suspended',
], 403);
}
// 4. 设置当前租户到请求上下文
$request->tenant = $tenant;
// 5. 设置数据库连接的租户作用域
$this->setTenantScope($tenant->id());
return $next($request);
}
private function setTenantScope(int $tenantId): void
{
// 全局设置租户 ID,用于查询过滤
app('tenant.context')->setCurrentTenantId($tenantId);
}
}4. 检查订阅中间件
php
<?php
declare(strict_types=1);
namespace app\middleware\subscription;
use app\contract\repository\SubscriptionRepositoryInterface;
use app\support\exception\SubscriptionExpiredException;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
final class CheckSubscriptionMiddleware implements MiddlewareInterface
{
public function __construct(
private readonly SubscriptionRepositoryInterface $subscriptionRepository
) {
}
public function process(Request $request, callable $next): Response
{
$tenant = $request->tenant;
if ($tenant === null) {
return json(['error' => 'Tenant not identified'], 400);
}
// 获取租户的活跃订阅
$subscription = $this->subscriptionRepository->findActivByTenantId($tenant->id());
if ($subscription === null || !$subscription->isActive()) {
throw new SubscriptionExpiredException('Subscription expired or not found');
}
// 设置订阅到请求上下文
$request->subscription = $subscription;
return $next($request);
}
}5. 配额检查服务
php
<?php
declare(strict_types=1);
namespace app\service\usage;
use app\contract\repository\UsageRepositoryInterface;
use app\contract\repository\SubscriptionRepositoryInterface;
use app\domain\usage\rule\QuotaEnforcementRule;
use app\support\exception\QuotaExceededException;
final class CheckQuotaService
{
public function __construct(
private readonly UsageRepositoryInterface $usageRepository,
private readonly SubscriptionRepositoryInterface $subscriptionRepository,
private readonly QuotaEnforcementRule $quotaRule
) {
}
public function check(int $tenantId, string $usageType): void
{
// 1. 获取租户订阅
$subscription = $this->subscriptionRepository->findActiveByTenantId($tenantId);
if ($subscription === null) {
throw new QuotaExceededException('No active subscription');
}
// 2. 获取套餐配额
$quota = $subscription->plan()->getQuota($usageType);
// 3. 获取当前用量
$currentUsage = $this->usageRepository->getCurrentMonthUsage($tenantId, $usageType);
// 4. 检查是否超出配额
if (!$this->quotaRule->isWithinLimit($currentUsage, $quota)) {
throw new QuotaExceededException(
"Quota exceeded for {$usageType}. Current: {$currentUsage}, Limit: {$quota}"
);
}
}
}6. 生成账单服务
php
<?php
declare(strict_types=1);
namespace app\service\billing;
use app\contract\repository\InvoiceRepositoryInterface;
use app\contract\repository\SubscriptionRepositoryInterface;
use app\contract\repository\UsageRepositoryInterface;
use app\domain\billing\entity\Invoice;
use app\domain\billing\vo\InvoiceNumber;
final class GenerateInvoiceService
{
public function __construct(
private readonly InvoiceRepositoryInterface $invoiceRepository,
private readonly SubscriptionRepositoryInterface $subscriptionRepository,
private readonly UsageRepositoryInterface $usageRepository
) {
}
public function handle(int $tenantId, \DateTimeImmutable $billingPeriodStart): Invoice
{
// 1. 获取订阅
$subscription = $this->subscriptionRepository->findActiveByTenantId($tenantId);
// 2. 计算基础费用
$baseAmount = $subscription->plan()->price();
// 3. 计算用量费用(如果有超出套餐的用量)
$usageAmount = $this->calculateUsageAmount($tenantId, $billingPeriodStart);
// 4. 创建发票
$invoice = Invoice::create(
invoiceNumber: InvoiceNumber::generate(),
tenantId: $tenantId,
subscriptionId: $subscription->id(),
baseAmount: $baseAmount,
usageAmount: $usageAmount,
billingPeriodStart: $billingPeriodStart,
billingPeriodEnd: $subscription->billingCycle()->calculateEndDate($billingPeriodStart)
);
// 5. 保存发票
$this->invoiceRepository->save($invoice);
return $invoice;
}
private function calculateUsageAmount(int $tenantId, \DateTimeImmutable $periodStart): int
{
// 计算超出套餐的用量费用
$overageUsage = $this->usageRepository->getOverageUsage($tenantId, $periodStart);
return $overageUsage * 100; // 每单位 $1
}
}7. 租户控制器
php
<?php
declare(strict_types=1);
namespace app\controller\api\v1;
use app\service\tenant\CreateTenantService;
use app\contract\repository\TenantRepositoryInterface;
use support\Request;
use support\Response;
final class TenantController
{
public function __construct(
private readonly CreateTenantService $createTenantService,
private readonly TenantRepositoryInterface $tenantRepository
) {
}
/**
* 创建租户(注册)
*/
public function create(Request $request): Response
{
$validated = $this->validate($request, [
'slug' => 'required|string|alpha_dash|unique:tenants',
'name' => 'required|string|max:100',
'email' => 'required|email',
'plan_id' => 'required|integer',
]);
$tenant = $this->createTenantService->handle(
slug: $validated['slug'],
name: $validated['name'],
email: $validated['email'],
planId: $validated['plan_id']
);
return json([
'success' => true,
'data' => [
'id' => $tenant->id(),
'slug' => $tenant->slug()->value(),
'subdomain' => $tenant->slug()->value() . '.example.com',
],
]);
}
/**
* 获取当前租户信息
*/
public function current(Request $request): Response
{
$tenant = $request->tenant;
return json([
'success' => true,
'data' => [
'id' => $tenant->id(),
'slug' => $tenant->slug()->value(),
'name' => $tenant->name(),
'status' => $tenant->status()->value(),
],
]);
}
}数据隔离策略
1. 全局作用域
php
<?php
namespace app\support\trait;
use Illuminate\Database\Eloquent\Builder;
trait BelongsToTenant
{
protected static function bootBelongsToTenant(): void
{
static::addGlobalScope('tenant', function (Builder $builder) {
$tenantId = app('tenant.context')->getCurrentTenantId();
if ($tenantId !== null) {
$builder->where('tenant_id', $tenantId);
}
});
static::creating(function ($model) {
if (!$model->tenant_id) {
$model->tenant_id = app('tenant.context')->getCurrentTenantId();
}
});
}
}2. 使用示例
php
<?php
namespace app\model\eloquent;
use Illuminate\Database\Eloquent\Model;
use app\support\trait\BelongsToTenant;
class Product extends Model
{
use BelongsToTenant;
protected $fillable = ['tenant_id', 'name', 'price'];
}定时任务示例
php
<?php
declare(strict_types=1);
namespace app\process\task;
use app\service\billing\GenerateInvoiceService;
use app\contract\repository\TenantRepositoryInterface;
use Workerman\Timer;
final class GenerateInvoicesTask
{
public function __construct(
private readonly GenerateInvoiceService $invoiceService,
private readonly TenantRepositoryInterface $tenantRepository
) {
}
public function onWorkerStart(): void
{
// 每天凌晨 1 点生成账单
Timer::add(86400, function () {
$this->generateMonthlyInvoices();
}, [], false);
}
private function generateMonthlyInvoices(): void
{
$today = new \DateTimeImmutable();
// 只在每月 1 号执行
if ((int) $today->format('d') !== 1) {
return;
}
$billingPeriodStart = $today->modify('-1 month');
$tenants = $this->tenantRepository->findAllActive();
foreach ($tenants as $tenant) {
try {
$this->invoiceService->handle($tenant->id(), $billingPeriodStart);
} catch (\Exception $e) {
logger()->error('Failed to generate invoice', [
'tenant_id' => $tenant->id(),
'error' => $e->getMessage(),
]);
}
}
}
}依赖注入配置
php
<?php
// config/container.php
use app\contract\repository\TenantRepositoryInterface;
use app\contract\repository\SubscriptionRepositoryInterface;
use app\contract\service\TenantResolverInterface;
use app\infrastructure\repository\eloquent\EloquentTenantRepository;
use app\infrastructure\repository\eloquent\EloquentSubscriptionRepository;
use app\infrastructure\service\SubdomainTenantResolver;
return [
TenantRepositoryInterface::class => EloquentTenantRepository::class,
SubscriptionRepositoryInterface::class => EloquentSubscriptionRepository::class,
TenantResolverInterface::class => SubdomainTenantResolver::class,
];最佳实践
1. 租户隔离
- 数据库级别隔离(Global Scope)
- 每个查询自动添加 tenant_id 过滤
- 防止跨租户数据访问
2. 订阅管理
- 订阅状态自动检查
- 过期订阅自动处理
- 升级降级平滑过渡
3. 计费系统
- 自动生成月度账单
- 用量超出自动计费
- 支付失败重试机制
4. 配额限制
- API 调用限流
- 存储空间限制
- 实时配额检查
5. 性能优化
- 租户信息缓存
- 订阅状态缓存
- 用量统计异步聚合