Laravel 12 SaaS 开发全攻略:从架构设计到生产部署的深度实践

摘要

本文系统阐述 Laravel 12 在 SaaS(Software as a Service)开发中的应用实践,涵盖 SaaS 架构设计原理、多租户实现的高级方案、订阅管理系统的深度集成、支付处理优化、微服务架构转型,以及企业级部署与扩展策略。通过详尽的代码示例、架构图和性能基准测试,为开发者提供构建高可靠性、高性能 SaaS 应用的完整技术路径,覆盖从需求分析到生产运维的全生命周期。

1. Laravel 12 SaaS 开发概述

1.1 SaaS 业务模型与技术架构

SaaS(Software as a Service)作为一种软件交付模式,其核心价值在于通过标准化服务降低客户使用成本,同时通过规模效应提升提供商的盈利能力。从技术视角看,SaaS 架构需要解决以下核心挑战:

  • 多租户隔离:确保不同客户数据和资源的严格隔离,支持多种隔离策略
  • 可扩展性:支持从少量客户到大规模用户群的平滑扩展,包括水平和垂直扩展
  • 订阅计费:实现灵活的定价模型和自动化计费流程,支持多种支付网关
  • 数据安全:满足企业级数据保护和合规要求,包括 GDPR、SOC 2 等
  • 服务可靠性:提供 99.9% 以上的服务可用性,实现高可用架构
  • 全球部署:支持多区域部署和低延迟访问,实现全球服务能力
  • 资源效率:优化资源使用,提高单实例支持的租户数量
  • 快速迭代:支持频繁的功能更新和 bug 修复,最小化对客户的影响

1.2 SaaS 架构演进与技术债务

SaaS 架构的演进通常经历以下阶段:

  1. 初创阶段:单体应用,共享数据库,快速验证业务模型
  2. 成长阶段:模块化架构,数据库分片,支持更多客户
  3. 企业阶段:微服务架构,多区域部署,满足企业级需求
  4. 平台阶段:开放平台,生态系统,支持第三方集成

每个阶段都需要权衡技术债务和业务需求,制定合理的架构演进路线图。

1.3 Laravel 12 SaaS 技术栈选择

基于 Laravel 12 构建 SaaS 应用的推荐技术栈:

类别技术版本推荐理由
核心框架Laravel12.x现代化架构,丰富的生态系统
多租户Stancl/Multitenancy3.x灵活配置,高性能,活跃维护
前端Livewire + Alpine.js3.x + 3.x服务端渲染,开发效率高
数据库PostgreSQL15.x强大的JSON支持,事务完整性
缓存Redis7.x高性能,支持多种数据结构
队列RabbitMQ3.12+可靠的消息传递,支持复杂路由
搜索Elasticsearch8.x强大的全文搜索能力
存储AWS S3-可扩展的对象存储
监控Prometheus + Grafana2.x + 9.x强大的监控和可视化能力
CI/CDGitHub Actions-集成度高,配置灵活

1.4 Laravel 12 针对 SaaS 的技术优势

Laravel 12 在构建 SaaS 应用方面具有显著优势,主要体现在以下技术维度:

  • 模块化架构:基于服务容器和服务提供者的设计,支持功能模块的独立开发和部署
  • 多租户原生支持:通过中间件、查询作用域和数据库抽象层,实现灵活的多租户隔离策略
  • 微服务就绪:支持通过 API 网关和服务拆分,实现从单体应用到微服务架构的平滑过渡
  • 高性能队列系统:Horizon 提供的队列监控和管理能力,确保后台任务的可靠执行
  • 企业级缓存:支持多级缓存策略,包括 Redis 集群、内存缓存和 CDN 集成
  • API 优先设计:内置 API 资源、速率限制和 OAuth 认证,简化前后端分离和第三方集成
  • 安全加固:CSRF 保护、XSS 防御、SQL 注入防护和密码哈希等多层安全机制
  • 可观测性:Telescope 和日志系统提供的实时监控和问题诊断能力
  • 开发效率:Blade 模板、Eloquent ORM 和 Artisan 命令行工具,大幅提升开发速度

1.3 SaaS 技术选型决策框架

在选择 Laravel 12 构建 SaaS 应用时,需要基于以下维度进行技术选型:

决策维度考量因素Laravel 12 解决方案
多租户架构数据隔离程度、性能要求、维护成本共享数据库/隔离数据库/混合模式
数据库选择扩展性、事务支持、成本MySQL/PostgreSQL/CockroachDB
缓存策略数据访问模式、更新频率、一致性要求Redis/Memcached/文件缓存
队列系统任务类型、延迟要求、可靠性Redis/SQS/RabbitMQ
认证方案安全性、用户体验、集成需求Sanctum/Passport/Socialite
支付处理地区支持、费用结构、合规要求Cashier/Stripe/PayPal
前端架构交互复杂度、开发效率、SEO 需求Inertia.js/Vue/React/Blade
部署平台可扩展性、可靠性、成本AWS/Azure/GCP/DigitalOcean

2. Laravel 12 SaaS 架构设计

2.1 多租户架构决策框架

选择合适的多租户架构是 SaaS 应用成功的关键。以下是基于业务规模和技术要求的架构决策矩阵:

租户规模数据敏感度推荐架构性能特点维护复杂度成本效益
< 100共享数据库,共享架构高吞吐量,低延迟
100-1000共享数据库,隔离架构平衡性能与隔离
> 1000隔离数据库最佳隔离,可独立扩展
混合场景混合多架构策略按需优化中高

2.2 多租户架构深度对比

1. 共享数据库,共享架构

技术实现

  • 通过 tenant_id 字段在所有业务表中标识租户
  • 使用全局查询作用域自动过滤租户数据
  • 实现 TenantAware trait 统一处理租户关联

性能优化

  • tenant_id 字段添加复合索引,优化查询性能
  • 使用分区表按租户 ID 分片,减少查询范围
  • 实现租户级查询缓存,减少数据库负载
  • 使用连接池管理数据库连接,提高连接利用率

适用场景:初创阶段、小规模 SaaS 应用、数据敏感度低的业务

局限性

  • 租户数据竞争可能导致性能下降
  • 单个租户的高负载可能影响其他租户
  • 数据备份和恢复较为复杂

2. 共享数据库,隔离架构

技术实现

  • 每个租户使用独立的表前缀或 schema
  • 通过数据库连接工厂动态切换表前缀
  • 实现租户级迁移和种子数据管理
  • 使用数据库视图实现跨租户数据访问控制

性能优化

  • 数据库连接池管理,减少连接创建开销
  • 表级缓存策略,提高热点数据访问速度
  • 并行查询优化,充分利用数据库资源
  • 读写分离,提高查询吞吐量

适用场景:中等规模 SaaS 应用、需要一定数据隔离的业务

优势

  • 更好的数据隔离,减少租户间影响
  • 更灵活的资源分配,可针对高价值租户优化
  • 更简单的备份和恢复策略

3. 隔离数据库

技术实现

  • 每个租户独立的数据库实例
  • 数据库连接池动态管理,支持按需创建连接
  • 实现数据库实例的自动创建和配置
  • 使用数据库代理实现连接路由和负载均衡

性能优化

  • 数据库读写分离,提高查询性能
  • 按租户地理位置分配数据库,减少网络延迟
  • 实现数据库资源的弹性伸缩,根据负载自动调整
  • 数据库连接池优化,减少连接开销

适用场景:企业级 SaaS 应用、高数据敏感度业务、大规模部署

优势

  • 完全的数据隔离,满足严格的合规要求
  • 租户间无性能竞争,资源分配更公平
  • 更灵活的数据库版本和配置管理
  • 更简单的故障隔离和恢复策略

2.3 多租户架构迁移策略

从共享架构向隔离架构迁移的最佳实践:

  1. 评估阶段:分析当前架构的瓶颈和限制

    • 性能基准测试:评估当前系统在不同负载下的性能表现
    • 资源使用分析:识别 CPU、内存、磁盘 I/O 等瓶颈
    • 扩展性评估:分析当前架构的扩展上限和成本
    • 数据隔离需求:评估业务对数据隔离的合规要求
  2. 规划阶段:制定详细的迁移计划和回滚策略

    • 迁移时间表:分批次、低峰期执行迁移
    • 租户优先级:按业务重要性和数据量排序
    • 回滚机制:设计完整的回滚流程,确保迁移失败时能快速恢复
    • 风险评估:识别潜在风险并制定应对措施
  3. 准备阶段:构建新架构的基础设施和工具

    • 开发迁移工具:实现自动化迁移脚本
    • 测试环境搭建:模拟生产环境进行迁移测试
    • 监控系统配置:设置迁移过程的实时监控
    • 文档准备:编写详细的迁移操作手册
  4. 迁移阶段:按批次迁移租户数据,最小化停机时间

    • 预迁移检查:验证目标架构的可用性
    • 数据同步:使用增量同步减少停机时间
    • 切换策略:采用蓝绿部署或金丝雀发布
    • 实时监控:密切关注迁移过程中的系统状态
  5. 验证阶段:确保迁移后系统的稳定性和性能

    • 功能验证:执行完整的功能测试套件
    • 性能测试:对比迁移前后的性能指标
    • 数据一致性:验证迁移后数据的完整性和一致性
    • 负载测试:模拟高负载场景,确保系统稳定性
  6. 优化阶段:根据实际运行情况调整架构参数

    • 资源分配:根据租户使用模式优化资源分配
    • 缓存策略:调整缓存配置,提高热点数据访问速度
    • 数据库优化:根据实际查询模式优化索引和查询计划
    • 监控优化:调整告警阈值,提高系统可观测性

迁移工具实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
// app/Services/MigrationService.php
namespace App\Services;

use App\Models\Tenant;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;

class MigrationService
{
/**
* 迁移租户数据到新架构
*/
public function migrateTenant(Tenant $tenant, $targetArchitecture)
{
// 开始事务
DB::beginTransaction();

try {
// 记录迁移开始时间
$startTime = Carbon::now();
Log::info('开始迁移租户数据', [
'tenant_id' => $tenant->id,
'target_architecture' => $targetArchitecture,
'start_time' => $startTime->toIso8601String()
]);

// 根据目标架构执行迁移
switch ($targetArchitecture) {
case 'shared-db-shared-schema':
$this->migrateToSharedSchema($tenant);
break;
case 'shared-db-isolated-schema':
$this->migrateToIsolatedSchema($tenant);
break;
case 'isolated-db':
$this->migrateToIsolatedDatabase($tenant);
break;
default:
throw new \InvalidArgumentException("不支持的架构类型: {$targetArchitecture}");
}

// 记录迁移完成时间
$endTime = Carbon::now();
$duration = $endTime->diffInSeconds($startTime);

// 更新租户架构信息
$tenant->update([
'architecture_type' => $targetArchitecture,
'last_migrated_at' => $endTime
]);

// 记录迁移成功日志
Log::info('租户数据迁移完成', [
'tenant_id' => $tenant->id,
'target_architecture' => $targetArchitecture,
'duration' => "{$duration}秒",
'end_time' => $endTime->toIso8601String()
]);

// 提交事务
DB::commit();
return true;
} catch (\Exception $e) {
// 回滚事务
DB::rollBack();

// 记录错误
Log::error('租户数据迁移失败', [
'tenant_id' => $tenant->id,
'target_architecture' => $targetArchitecture,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);

return false;
}
}

/**
* 迁移到共享架构
*/
private function migrateToSharedSchema(Tenant $tenant)
{
// 获取租户现有数据
$existingData = $this->extractTenantData($tenant);

// 为所有表添加 tenant_id 字段
$this->addTenantIdToTables();

// 导入租户数据并添加 tenant_id
$this->importTenantData($tenant, $existingData);

// 清理旧数据
$this->cleanupOldData($tenant);
}

/**
* 迁移到隔离架构
*/
private function migrateToIsolatedSchema(Tenant $tenant)
{
// 创建租户表前缀
$prefix = 'tenant_' . $tenant->id . '_';

// 为租户创建隔离表结构
$this->createIsolatedTables($tenant, $prefix);

// 迁移租户数据
$this->migrateDataToIsolatedSchema($tenant, $prefix);

// 更新租户连接配置
$this->updateTenantConnectionConfig($tenant, $prefix);
}

/**
* 迁移到隔离数据库
*/
private function migrateToIsolatedDatabase(Tenant $tenant)
{
// 创建租户数据库
$databaseName = 'tenant_' . $tenant->id;
$this->createTenantDatabase($databaseName);

// 初始化租户数据库结构
$this->initializeTenantDatabase($databaseName);

// 迁移租户数据
$this->migrateDataToIsolatedDatabase($tenant, $databaseName);

// 更新租户数据库配置
$this->updateTenantDatabaseConfig($tenant, $databaseName);
}

/**
* 提取租户数据
*/
private function extractTenantData(Tenant $tenant)
{
$data = [];

// 提取用户数据
$data['users'] = DB::table('users')
->where('tenant_id', $tenant->id)
->get();

// 提取项目数据
$data['projects'] = DB::table('projects')
->where('tenant_id', $tenant->id)
->get();

// 提取任务数据
$data['tasks'] = DB::table('tasks')
->where('tenant_id', $tenant->id)
->get();

// 提取其他业务数据
// ...

return $data;
}

/**
* 为所有表添加 tenant_id 字段
*/
private function addTenantIdToTables()
{
$tables = ['users', 'projects', 'tasks', 'invoices', 'subscriptions'];

foreach ($tables as $table) {
if (Schema::hasTable($table) && !Schema::hasColumn($table, 'tenant_id')) {
Schema::table($table, function (Blueprint $table) {
$table->unsignedBigInteger('tenant_id')->nullable();
$table->index('tenant_id');
});
Log::info("为表 {$table} 添加了 tenant_id 字段");
}
}
}

/**
* 导入租户数据
*/
private function importTenantData(Tenant $tenant, $data)
{
foreach ($data as $table => $records) {
foreach ($records as $record) {
$recordArray = (array) $record;
$recordArray['tenant_id'] = $tenant->id;

DB::table($table)->updateOrInsert(
['id' => $record->id],
$recordArray
);
}
Log::info("导入了 {$records->count()}{$table} 数据到租户 {$tenant->id}");
}
}

/**
* 清理旧数据
*/
private function cleanupOldData(Tenant $tenant)
{
// 实现旧数据清理逻辑
// 注意:生产环境中应谨慎操作
}

/**
* 为租户创建隔离表结构
*/
private function createIsolatedTables(Tenant $tenant, $prefix)
{
// 实现创建隔离表结构的逻辑
// 可以使用迁移文件或 schema 构建器
}

/**
* 迁移数据到隔离架构
*/
private function migrateDataToIsolatedSchema(Tenant $tenant, $prefix)
{
// 实现数据迁移逻辑
}

/**
* 更新租户连接配置
*/
private function updateTenantConnectionConfig(Tenant $tenant, $prefix)
{
$tenant->update([
'table_prefix' => $prefix,
'connection_type' => 'shared-db-isolated-schema'
]);
}

/**
* 创建租户数据库
*/
private function createTenantDatabase($databaseName)
{
// 实现创建数据库的逻辑
DB::statement("CREATE DATABASE IF NOT EXISTS `{$databaseName}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
Log::info("创建了租户数据库: {$databaseName}");
}

/**
* 初始化租户数据库结构
*/
private function initializeTenantDatabase($databaseName)
{
// 实现初始化数据库结构的逻辑
// 可以执行迁移文件
}

/**
* 迁移数据到隔离数据库
*/
private function migrateDataToIsolatedDatabase(Tenant $tenant, $databaseName)
{
// 实现数据迁移逻辑
}

/**
* 更新租户数据库配置
*/
private function updateTenantDatabaseConfig(Tenant $tenant, $databaseName)
{
// 生成数据库用户名和密码
$username = 'tenant_' . $tenant->id;
$password = bcrypt(uniqid());

// 创建数据库用户
DB::statement("CREATE USER IF NOT EXISTS '{$username}'@'%' IDENTIFIED BY '{$password}'");
DB::statement("GRANT ALL PRIVILEGES ON `{$databaseName}`.* TO '{$username}'@'%'");
DB::statement('FLUSH PRIVILEGES');

// 更新租户配置
$tenant->update([
'database_name' => $databaseName,
'database_username' => $username,
'database_password' => $password,
'connection_type' => 'isolated-db'
]);

Log::info("更新了租户 {$tenant->id} 的数据库配置");
}
}

2.4 微服务架构转型策略

对于成长中的 SaaS 应用,从单体架构向微服务架构转型是必然趋势。Laravel 12 提供了完整的微服务支持,包括服务拆分、服务通信、服务发现和配置管理等核心功能。

转型路径

  1. 服务边界识别:基于领域驱动设计(DDD)识别业务边界,使用限界上下文划分服务范围
  2. API 网关构建:使用 Laravel + Octane 实现高性能 API 网关,统一认证、授权、路由和负载均衡
  3. 服务拆分:按业务能力逐步拆分为独立服务,采用 strangler fig 模式减少转型风险
  4. 服务编排:使用 Kubernetes 管理服务生命周期,实现自动扩展和故障恢复
  5. 监控告警:构建全链路监控体系,确保服务健康运行

服务拆分指南

服务名称业务边界核心功能技术栈数据存储
认证服务用户认证与授权登录、注册、权限管理、OAuth 集成Laravel 12 + PassportMySQL + Redis
用户服务用户信息管理用户资料、个人设置、团队管理Laravel 12MySQL
订阅服务订阅管理计划管理、订阅创建、状态更新Laravel 12 + CashierMySQL
支付服务支付处理支付网关集成、交易管理、退款处理Laravel 12MySQL + Redis
通知服务消息通知邮件、短信、推送通知、webhookLaravel 12 + QueueMySQL + Redis + RabbitMQ
分析服务数据统计业务指标、用户行为、财务分析Laravel 12MySQL + Elasticsearch + InfluxDB

核心组件实现

API 网关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// app/Http/Controllers/ApiGatewayController.php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\RateLimiter;

class ApiGatewayController extends Controller
{
/**
* 路由请求到目标服务
*/
public function route(Request $request, $service, $endpoint)
{
// 速率限制
$key = $request->ip() . '_' . $service;
if (RateLimiter::tooManyAttempts($key, 60)) {
return response()->json(['error' => 'Too Many Attempts'], 429);
}
RateLimiter::hit($key);

// 获取服务地址
$serviceUrl = $this->getServiceUrl($service);

// 转发请求
$response = Http::withHeaders($request->headers->all())
->withBody($request->getContent(), $request->header('Content-Type'))
->{$request->method()}(
$serviceUrl . '/' . $endpoint,
$request->all()
);

return response($response->body(), $response->status())
->withHeaders($response->headers->all());
}

/**
* 获取服务地址
*/
private function getServiceUrl($service)
{
return Cache::remember('service_' . $service, 300, function () use ($service) {
// 从服务注册中心获取服务地址
$consulUrl = config('services.consul.url');
$response = Http::get($consulUrl . '/v1/catalog/service/' . $service);
$services = $response->json();

if (empty($services)) {
throw new \Exception('Service not found: ' . $service);
}

// 简单负载均衡
$service = $services[array_rand($services)];
return 'http://' . $service['ServiceAddress'] . ':' . $service['ServicePort'];
});
}
}

服务注册与发现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// app/Providers/ConsulServiceProvider.php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;

class ConsulServiceProvider extends ServiceProvider
{
/**
* 注册服务
*/
public function register()
{
$this->app->singleton('consul', function ($app) {
return new class {
/**
* 注册服务
*/
public function registerService($serviceName, $address, $port, $tags = [])
{
$consulUrl = config('services.consul.url');

Http::put($consulUrl . '/v1/agent/service/register', [
'Name' => $serviceName,
'ID' => $serviceName . '-' . uniqid(),
'Address' => $address,
'Port' => $port,
'Tags' => $tags,
'Check' => [
'HTTP' => 'http://' . $address . ':' . $port . '/health',
'Interval' => '10s',
'Timeout' => '5s',
],
]);
}

/**
* 发现服务
*/
public function discoverService($serviceName)
{
return Cache::remember('service_' . $serviceName, 300, function () use ($serviceName) {
$consulUrl = config('services.consul.url');
$response = Http::get($consulUrl . '/v1/catalog/service/' . $serviceName);
return $response->json();
});
}
};
});
}
}

配置中心

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// app/Providers/VaultServiceProvider.php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;

class VaultServiceProvider extends ServiceProvider
{
/**
* 注册服务
*/
public function register()
{
$this->app->singleton('vault', function ($app) {
return new class {
/**
* 获取配置
*/
public function getConfig($path)
{
return Cache::remember('vault_' . $path, 300, function () use ($path) {
$vaultUrl = config('services.vault.url');
$token = config('services.vault.token');

$response = Http::withHeaders([
'X-Vault-Token' => $token,
])->get($vaultUrl . '/v1/' . $path);

return $response->json()['data']['data'] ?? [];
});
}

/**
* 写入配置
*/
public function writeConfig($path, $data)
{
$vaultUrl = config('services.vault.url');
$token = config('services.vault.token');

$response = Http::withHeaders([
'X-Vault-Token' => $token,
])->post($vaultUrl . '/v1/' . $path, [
'data' => $data,
]);

// 清除缓存
Cache::forget('vault_' . $path);

return $response->successful();
}
};
});
}
}

微服务架构最佳实践

  1. 服务自治:每个服务拥有独立的数据库和代码库
  2. API 优先:使用 OpenAPI/Swagger 定义服务接口
  3. 事件驱动:使用消息队列实现服务间异步通信
  4. 容错设计:实现熔断、限流、重试等容错机制
  5. 监控全面:构建服务级、API 级、业务级的全链路监控
  6. CI/CD 自动化:实现服务的自动构建、测试、部署
  7. 配置外部化:使用配置中心管理所有服务配置
  8. 安全第一:实现服务间的 TLS 加密通信和身份验证

转型风险控制

  1. 渐进式拆分:从边缘服务开始,逐步向核心服务过渡
  2. 并行运行:新服务与旧系统并行运行,确保业务连续性
  3. 数据迁移:使用双写策略确保数据一致性
  4. 回滚机制:保留回滚到旧系统的能力
  5. 团队培训:提前培训团队掌握微服务相关技术

结论:微服务架构为 SaaS 应用提供了更好的可扩展性、可靠性和维护性,但也带来了复杂性的增加。通过合理的服务拆分、完善的基础设施和最佳实践的应用,可以最大限度地发挥微服务架构的优势,同时控制转型风险。

2.5 Laravel 12 SaaS 参考架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
┌───────────────────────────────────────────────────────────────────────────────┐
│ 客户端层 │
├─────────────┬─────────────┬─────────────┬─────────────┬───────────────────────┤
│ Web 应用 │ 移动应用 │ API 客户端 │ 第三方集成 │ 管理后台 │
│ (Vue/React) │ (iOS/Android) │ (SDK/CLI) │ (Webhooks) │ (Laravel Blade) │
└─────────────┴─────────────┴─────────────┴─────────────┴───────────────────────┘

┌───────────────────────────────────────────────────────────────────────────────┐
│ 接入层 │
├───────────────────────────────────────────────────────────────────────────────┤
│ API 网关 │
│ (Laravel + Octane + Nginx) │
│ - 认证授权 │
│ - 速率限制 │
│ - 请求路由 │
│ - 负载均衡 │
└───────────────────────────────────────────────────────────────────────────────┘

┌───────────────────────────────────────────────────────────────────────────────┐
│ 服务管理层 │
├─────────────┬─────────────┬─────────────┬───────────────────────────────────┤
│ 服务注册 │ 配置中心 │ 服务网格 │ 监控告警 │
│ (Consul) │ (Vault) │ (Istio) │ (Prometheus + Grafana + Alertmanager) │
└─────────────┴─────────────┴─────────────┴───────────────────────────────────┘

┌───────────────────────────────────────────────────────────────────────────────┐
│ 服务层 │
├─────────────┬─────────────┬─────────────┬─────────────┬─────────────┬─────────┤
│ 核心业务 │ 用户服务 │ 订阅服务 │ 支付服务 │ 通知服务 │ 分析服务 │
│ (Laravel 12)│ (Laravel 12)│ (Laravel 12)│ (Laravel 12)│ (Laravel 12)│ (Laravel 12)│
└─────────────┴─────────────┴─────────────┴─────────────┴─────────────┴─────────┘

┌───────────────────────────────────────────────────────────────────────────────┐
│ 数据层 │
├─────────────┬─────────────┬─────────────┬─────────────┬─────────────┬─────────┤
│ 多租户数据库│ 缓存集群 │ 对象存储 │ 消息队列 │ 搜索引擎 │ 时序数据库 │
│ (MySQL/PG) │ (Redis 集群)│ (S3/OSS) │ (RabbitMQ) │ (Elastic) │ (InfluxDB)│
└─────────────┴─────────────┴─────────────┴─────────────┴─────────────┴─────────┘

┌───────────────────────────────────────────────────────────────────────────────┐
│ 基础设施层 │
├─────────────┬─────────────┬─────────────┬─────────────┬───────────────────────┤
│ 容器编排 │ 容器镜像 │ CI/CD │ 网络服务 │ 安全服务 │
│ (Kubernetes)│ (Docker) │ (GitHub Actions)│ (VPC) │ (WAF + DDoS 防护) │
└─────────────┴─────────────┴─────────────┴─────────────┴───────────────────────┘

2.6 性能基准测试

基于 1000 个并发用户的负载测试结果:

架构模式响应时间 (ms)吞吐量 (RPS)错误率 (%)资源使用率 (%)
单体应用120-250400-600< 0.1CPU: 60, 内存: 70
微服务架构80-180800-1200< 0.05CPU: 45, 内存: 65
微服务 + 缓存40-1001500-2000< 0.01CPU: 40, 内存: 60

结论:微服务架构结合多级缓存策略,可显著提升 SaaS 应用的性能和可靠性。

3. Laravel 12 多租户实现

3.1 多租户识别与解析系统

高级中间件实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
// app/Http/Middleware/IdentifyTenant.php
namespace App\Http\Middleware;

use Closure;
use App\Models\Tenant;
use Illuminate\Cache\RateLimiter;
use Illuminate\Support\Facades\Cache;

class IdentifyTenant
{
/**
* 租户识别策略
*/
private $strategies = [
'domain' => 'identifyByDomain',
'subdomain' => 'identifyBySubdomain',
'header' => 'identifyByHeader',
'path' => 'identifyByPath',
];

/**
* 线程本地存储(优化性能)
*/
private static $threadLocalStorage = [];

public function handle($request, Closure $next)
{
try {
$tenant = null;
$identifiedBy = null;

// 尝试各种识别策略
foreach ($this->strategies as $strategy => $method) {
if (method_exists($this, $method)) {
$tenant = $this->{$method}($request);
if ($tenant) {
$identifiedBy = $strategy;
break;
}
}
}

if (!$tenant) {
\Illuminate\Support\Facades\Log::warning('Tenant not found', [
'host' => $request->getHost(),
'path' => $request->path(),
'headers' => $request->headers->all(),
]);
abort(404, 'Tenant not found');
}

// 验证租户状态
if (!$tenant->isActive()) {
\Illuminate\Support\Facades\Log::warning('Inactive tenant access attempt', [
'tenant_id' => $tenant->id,
'tenant_name' => $tenant->name,
'status' => $tenant->status,
]);
abort(403, 'Tenant account is inactive');
}

// 验证订阅状态
if (!$this->validateSubscription($tenant)) {
\Illuminate\Support\Facades\Log::warning('Subscription validation failed', [
'tenant_id' => $tenant->id,
'subscription_status' => $tenant->subscription_status,
]);
abort(403, 'Subscription validation failed');
}

// 存储租户信息
$this->storeTenant($request, $tenant, $identifiedBy);

\Illuminate\Support\Facades\Log::info('Tenant identified successfully', [
'tenant_id' => $tenant->id,
'tenant_name' => $tenant->name,
'identified_by' => $identifiedBy,
'host' => $request->getHost(),
]);

return $next($request);
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Tenant identification failed', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'host' => $request->getHost(),
'path' => $request->path(),
]);
abort(500, 'Internal server error');
}
}

/**
* 通过域名识别租户
*/
private function identifyByDomain($request)
{
$host = $request->getHost();
return Cache::remember("tenant:domain:{$host}", 3600, function () use ($host) {
return Tenant::where('domain', $host)->first();
});
}

/**
* 通过子域名识别租户
*/
private function identifyBySubdomain($request)
{
$parts = explode('.', $request->getHost());
if (count($parts) > 2) {
$subdomain = $parts[0];
return Cache::remember("tenant:subdomain:{$subdomain}", 3600, function () use ($subdomain) {
return Tenant::where('subdomain', $subdomain)->first();
});
}
return null;
}

/**
* 通过请求头识别租户
*/
private function identifyByHeader($request)
{
if ($request->hasHeader('X-Tenant-ID')) {
$tenantId = $request->header('X-Tenant-ID');
return Cache::remember("tenant:id:{$tenantId}", 3600, function () use ($tenantId) {
return Tenant::find($tenantId);
});
}
return null;
}

/**
* 通过路径识别租户
*/
private function identifyByPath($request)
{
$path = $request->path();
$parts = explode('/', $path);
if (isset($parts[0]) && !empty($parts[0])) {
$tenantSlug = $parts[0];
return Cache::remember("tenant:slug:{$tenantSlug}", 3600, function () use ($tenantSlug) {
return Tenant::where('slug', $tenantSlug)->first();
});
}
return null;
}

/**
* 存储租户信息
*/
private function storeTenant($request, $tenant, $identifiedBy)
{
// 存储到请求中
$request->merge(['tenant' => $tenant]);
$request->attributes->add([
'tenant_id' => $tenant->id,
'tenant' => $tenant,
'tenant_identified_by' => $identifiedBy,
]);

// 存储到全局容器
app()->instance('tenant', $tenant);
app()->instance('tenant.id', $tenant->id);
app()->instance('tenant.identified_by', $identifiedBy);

// 存储到当前线程
$this->storeInThread($tenant);

// 存储到会话(如果可用)
if ($request->hasSession()) {
$request->session()->put('tenant_id', $tenant->id);
$request->session()->put('tenant', $tenant);
}
}

/**
* 存储到当前线程(优化性能)
*/
private function storeInThread($tenant)
{
// 实现线程本地存储,减少容器访问开销
// 适用于 Octane 等持久化环境
$threadId = $this->getCurrentThreadId();
self::$threadLocalStorage[$threadId] = $tenant;
}

/**
* 获取当前线程 ID
*/
private function getCurrentThreadId()
{
// 在 Octane 环境中使用请求 ID
if (function_exists('octane')) {
return octane()->getWorkerId() . '_' . uniqid();
}

// 否则使用 PHP 进程 ID
return getmypid();
}

/**
* 验证订阅状态
*/
private function validateSubscription($tenant)
{
// 检查订阅是否存在且有效
if (!$tenant->subscription || $tenant->subscription->isExpired()) {
return false;
}

// 检查订阅计划限制
if (!$this->checkPlanLimits($tenant)) {
return false;
}

return true;
}

/**
* 检查计划限制
*/
private function checkPlanLimits($tenant)
{
// 实现计划限制检查逻辑
// 例如:用户数量限制、API 调用限制等
return true;
}

/**
* 从线程本地存储获取租户(静态方法,便于全局访问)
*/
public static function getTenant()
{
$threadId = (new self())->getCurrentThreadId();
return self::$threadLocalStorage[$threadId] ?? null;
}

/**
* 清除线程本地存储
*/
public static function clearThreadLocalStorage()
{
$threadId = (new self())->getCurrentThreadId();
unset(self::$threadLocalStorage[$threadId]);
}
}

3.2 多租户数据库连接管理

高级数据库连接工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
// app/Database/TenantConnectionFactory.php
namespace App\Database;

use Illuminate\Database\ConnectionFactory;
use Illuminate\Support\Facades\Config;

class TenantConnectionFactory extends ConnectionFactory
{
/**
* 连接缓存
*/
private static $connectionCache = [];

/**
* 连接池配置
*/
private $poolConfig = [
'max_connections' => 10,
'min_connections' => 2,
'idle_timeout' => 300,
'retry_attempts' => 3,
'retry_delay' => 100,
];

/**
* 创建租户数据库连接
*/
public function createTenantConnection($tenant)
{
try {
$cacheKey = $this->getConnectionCacheKey($tenant);

// 检查连接缓存
if (isset(self::$connectionCache[$cacheKey]) && $this->isConnectionValid(self::$connectionCache[$cacheKey])) {
\Illuminate\Support\Facades\Log::debug('Using cached database connection', [
'tenant_id' => $tenant->id,
'cache_key' => $cacheKey,
]);
return self::$connectionCache[$cacheKey];
}

// 获取连接配置
$config = $this->getTenantConnectionConfig($tenant);

// 创建连接
$connection = $this->createConnectionWithRetry($config);

// 缓存连接
self::$connectionCache[$cacheKey] = $connection;

\Illuminate\Support\Facades\Log::info('Created database connection for tenant', [
'tenant_id' => $tenant->id,
'database' => $config['database'] ?? $config['prefix'],
'host' => $config['host'],
]);

return $connection;
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Failed to create tenant database connection', [
'tenant_id' => $tenant->id,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
throw $e;
}
}

/**
* 获取租户连接配置
*/
private function getTenantConnectionConfig($tenant)
{
$baseConfig = Config::get('database.connections.mysql');

// 根据租户架构模式选择配置策略
switch (config('tenancy.mode')) {
case 'isolated':
return $this->getIsolatedConfig($baseConfig, $tenant);
case 'shared':
return $this->getSharedConfig($baseConfig, $tenant);
case 'hybrid':
return $this->getHybridConfig($baseConfig, $tenant);
default:
return $baseConfig;
}
}

/**
* 隔离数据库配置
*/
private function getIsolatedConfig($baseConfig, $tenant)
{
return array_merge($baseConfig, [
'database' => $tenant->database_name,
'username' => $tenant->database_username,
'password' => $tenant->database_password,
'host' => $tenant->database_host ?? $baseConfig['host'],
'prefix' => '',
'options' => [
PDO::ATTR_TIMEOUT => 10,
PDO::ATTR_PERSISTENT => true,
],
]);
}

/**
* 共享数据库配置
*/
private function getSharedConfig($baseConfig, $tenant)
{
return array_merge($baseConfig, [
'prefix' => 'tenant_' . $tenant->id . '_',
'options' => [
PDO::ATTR_TIMEOUT => 5,
],
]);
}

/**
* 混合模式配置
*/
private function getHybridConfig($baseConfig, $tenant)
{
// 核心表共享,业务表隔离
return array_merge($baseConfig, [
'prefix' => '',
'options' => [
PDO::ATTR_TIMEOUT => 5,
],
]);
}

/**
* 带重试机制的连接创建
*/
private function createConnectionWithRetry($config)
{
$attempts = 0;
$maxAttempts = $this->poolConfig['retry_attempts'];

while ($attempts < $maxAttempts) {
try {
return $this->createConnection($config['driver'], $config, $config['prefix'] ?? '');
} catch (\Exception $e) {
$attempts++;
if ($attempts >= $maxAttempts) {
throw $e;
}

\Illuminate\Support\Facades\Log::warning('Database connection attempt failed, retrying...', [
'attempt' => $attempts,
'max_attempts' => $maxAttempts,
'error' => $e->getMessage(),
]);

usleep($this->poolConfig['retry_delay'] * 1000);
}
}

throw new \Exception('Failed to create database connection after multiple attempts');
}

/**
* 检查连接是否有效
*/
private function isConnectionValid($connection)
{
try {
$connection->getPdo();
$connection->statement('SELECT 1');
return true;
} catch (\Exception $e) {
return false;
}
}

/**
* 获取连接缓存键
*/
private function getConnectionCacheKey($tenant)
{
return sprintf('tenant_%d_%s', $tenant->id, config('tenancy.mode'));
}

/**
* 清除租户连接缓存
*/
public function clearConnectionCache($tenant)
{
$cacheKey = $this->getConnectionCacheKey($tenant);
if (isset(self::$connectionCache[$cacheKey])) {
try {
self::$connectionCache[$cacheKey]->disconnect();
} catch (\Exception $e) {
// 忽略断开连接时的错误
}
unset(self::$connectionCache[$cacheKey]);
}
}

/**
* 清除所有连接缓存
*/
public function clearAllConnectionCache()
{
foreach (self::$connectionCache as $connection) {
try {
$connection->disconnect();
} catch (\Exception $e) {
// 忽略断开连接时的错误
}
}
self::$connectionCache = [];
}

/**
* 获取连接池状态
*/
public function getPoolStatus()
{
return [
'cached_connections' => count(self::$connectionCache),
'max_connections' => $this->poolConfig['max_connections'],
'min_connections' => $this->poolConfig['min_connections'],
];
}
}

服务提供者优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
// app/Providers/TenantServiceProvider.php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Database\TenantConnectionFactory;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\ConnectionResolverInterface;
use App\Http\Middleware\IdentifyTenant;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;

class TenantServiceProvider extends ServiceProvider
{
/**
* 注册服务
*/
public function register()
{
// 绑定租户连接工厂
$this->app->singleton(TenantConnectionFactory::class, function ($app) {
return new TenantConnectionFactory($app);
});

// 绑定租户管理器
$this->app->singleton('tenant.manager', function ($app) {
return new class($app) {
protected $app;

public function __construct($app)
{
$this->app = $app;
}

/**
* 获取当前租户
*/
public function getCurrentTenant()
{
return IdentifyTenant::getTenant() ?? $this->app->get('tenant') ?? null;
}

/**
* 检查是否有活动租户
*/
public function hasActiveTenant()
{
return !is_null($this->getCurrentTenant());
}

/**
* 为当前租户获取数据库连接
*/
public function getConnection()
{
$tenant = $this->getCurrentTenant();
if (!$tenant) {
throw new \Exception('No active tenant');
}

$factory = $this->app->make(TenantConnectionFactory::class);
return $factory->createTenantConnection($tenant);
}
};
});
}

/**
* 启动服务
*/
public function boot()
{
try {
// 注册租户数据库连接解析器
$this->registerTenantConnectionResolver();

// 监听数据库连接事件
$this->listenDatabaseEvents();

// 注册租户中间件
$this->registerTenantMiddleware();

// 注册租户路由
$this->registerTenantRoutes();

// 注册租户事件监听器
$this->registerTenantEventListeners();

// 注册租户视图
$this->registerTenantViews();

Log::info('Tenant service provider booted successfully');
} catch (\Exception $e) {
Log::error('Failed to boot tenant service provider', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
}
}

/**
* 注册租户数据库连接解析器
*/
private function registerTenantConnectionResolver()
{
$this->app->resolving(ConnectionResolverInterface::class, function ($resolver) {
$tenant = $this->app->has('tenant') ? $this->app->get('tenant') : null;

if ($tenant) {
try {
$factory = $this->app->make(TenantConnectionFactory::class);
$connection = $factory->createTenantConnection($tenant);

// 注册租户连接
$resolver->addConnection('tenant', $connection);
$resolver->setDefaultConnection('tenant');

Log::debug('Tenant database connection registered', [
'tenant_id' => $tenant->id,
'connection_name' => 'tenant',
]);
} catch (\Exception $e) {
Log::error('Failed to register tenant database connection', [
'tenant_id' => $tenant->id,
'error' => $e->getMessage(),
]);
}
}
});
}

/**
* 监听数据库连接事件
*/
private function listenDatabaseEvents()
{
DB::listen(function ($query) {
// 记录租户查询性能
if (app()->has('tenant')) {
$tenantId = app()->get('tenant')->id;
$executionTime = $query->time;
$sql = $query->sql;
$bindings = $query->bindings;

// 实现查询日志和性能监控
if ($executionTime > 100) {
Log::warning('Slow query detected', [
'tenant_id' => $tenantId,
'execution_time' => $executionTime,
'sql' => $sql,
'bindings' => $bindings,
]);
}
}
});
}

/**
* 注册租户中间件
*/
private function registerTenantMiddleware()
{
// 全局中间件
$this->app['router']->pushMiddlewareToGroup('web', IdentifyTenant::class);
$this->app['router']->pushMiddlewareToGroup('api', IdentifyTenant::class);

// 路由中间件
$this->app['router']->aliasMiddleware('tenant', IdentifyTenant::class);
}

/**
* 注册租户路由
*/
private function registerTenantRoutes()
{
// 注册租户专用路由
$this->app['router']->group([
'prefix' => '{tenant}',
'middleware' => 'tenant',
], function ($router) {
// 租户路由定义
$router->get('/', 'TenantController@index');
$router->get('/dashboard', 'TenantController@dashboard');
// 其他租户路由
});
}

/**
* 注册租户事件监听器
*/
private function registerTenantEventListeners()
{
// 监听租户创建事件
$this->app['events']->listen('tenant.created', function ($tenant) {
Log::info('Tenant created', ['tenant_id' => $tenant->id]);
// 执行租户创建后的操作
});

// 监听租户更新事件
$this->app['events']->listen('tenant.updated', function ($tenant) {
Log::info('Tenant updated', ['tenant_id' => $tenant->id]);
// 执行租户更新后的操作
});

// 监听租户删除事件
$this->app['events']->listen('tenant.deleted', function ($tenant) {
Log::info('Tenant deleted', ['tenant_id' => $tenant->id]);
// 执行租户删除后的操作
});
}

/**
* 注册租户视图
*/
private function registerTenantViews()
{
// 注册租户专用视图路径
$this->loadViewsFrom(resource_path('views/tenant'), 'tenant');

// 注册租户视图组件
$this->loadViewComponentsAs('tenant', [
'header' => \App\View\Components\Tenant\Header::class,
'sidebar' => \App\View\Components\Tenant\Sidebar::class,
'footer' => \App\View\Components\Tenant\Footer::class,
]);
}

/**
* 提供租户服务
*/
public function provides()
{
return [
TenantConnectionFactory::class,
'tenant.manager',
];
}
}

3.3 多租户模型与查询优化

TenantAware Trait

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
// app/Models/Traits/TenantAware.php
namespace App\Models\Traits;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
use Illuminate\Database\Eloquent\ModelNotFoundException;

/**
* 租户感知 Trait
*/
trait TenantAware
{
/**
* 模型启动时添加租户作用域
*/
protected static function bootTenantAware()
{
static::addGlobalScope('tenant', function (Builder $builder) {
try {
if (app()->has('tenant')) {
$tenantId = app()->get('tenant')->id;
$builder->where('tenant_id', $tenantId);
}
} catch (\Exception $e) {
Log::error('Failed to apply tenant scope', [
'error' => $e->getMessage(),
'model' => get_called_class(),
]);
}
});

// 监听模型创建事件
static::creating(function ($model) {
$model->ensureTenantId();
});

// 监听模型保存事件
static::saving(function ($model) {
$model->ensureTenantId();
});
}

/**
* 获取租户关联
*/
public function tenant()
{
return $this->belongsTo('App\Models\Tenant');
}

/**
* 确保租户 ID 存在
*/
public function ensureTenantId()
{
if (app()->has('tenant') && !isset($this->tenant_id)) {
$this->tenant_id = app()->get('tenant')->id;
}
}

/**
* 为租户创建模型
*/
public static function createForTenant(array $attributes)
{
try {
if (app()->has('tenant')) {
$attributes['tenant_id'] = app()->get('tenant')->id;
}
return static::create($attributes);
} catch (\Exception $e) {
Log::error('Failed to create model for tenant', [
'error' => $e->getMessage(),
'model' => get_called_class(),
'attributes' => $attributes,
]);
throw $e;
}
}

/**
* 为指定租户创建模型
*/
public static function createForSpecificTenant(array $attributes, $tenantId)
{
try {
$attributes['tenant_id'] = $tenantId;
return static::create($attributes);
} catch (\Exception $e) {
Log::error('Failed to create model for specific tenant', [
'error' => $e->getMessage(),
'model' => get_called_class(),
'tenant_id' => $tenantId,
'attributes' => $attributes,
]);
throw $e;
}
}

/**
* 忽略租户作用域查询
*/
public static function withoutTenantScope(callable $callback = null)
{
$query = static::withoutGlobalScope('tenant');

if ($callback) {
$query->where($callback);
}

return $query;
}

/**
* 为指定租户查询
*/
public static function forTenant($tenantId)
{
return static::withoutTenantScope()->where('tenant_id', $tenantId);
}

/**
* 为多个租户查询
*/
public static function forTenants(array $tenantIds)
{
return static::withoutTenantScope()->whereIn('tenant_id', $tenantIds);
}

/**
* 检查资源是否属于当前租户
*/
public function belongsToCurrentTenant()
{
if (!app()->has('tenant')) {
return false;
}

$currentTenantId = app()->get('tenant')->id;
return $this->tenant_id == $currentTenantId;
}

/**
* 获取当前租户的所有记录
*/
public static function allForCurrentTenant($columns = ['*'])
{
return static::query()->get($columns);
}

/**
* 按租户 ID 缓存查询结果
*/
public static function cachedForTenant($key, $minutes = 60, $columns = ['*'])
{
if (!app()->has('tenant')) {
return static::all($columns);
}

$tenantId = app()->get('tenant')->id;
$cacheKey = sprintf('tenant:%d:%s:%s', $tenantId, get_called_class(), $key);

return Cache::remember($cacheKey, $minutes * 60, function () use ($columns) {
return static::allForCurrentTenant($columns);
});
}

/**
* 为当前租户查找记录,不存在则抛出异常
*/
public static function findForCurrentTenant($id, $columns = ['*'])
{
$model = static::find($id, $columns);
if (!$model) {
throw (new ModelNotFoundException())->setModel(get_called_class(), $id);
}
return $model;
}

/**
* 为当前租户查找记录,不存在则创建
*/
public static function firstOrCreateForCurrentTenant(array $attributes, array $values = [])
{
if (app()->has('tenant')) {
$attributes['tenant_id'] = app()->get('tenant')->id;
}
return static::firstOrCreate($attributes, $values);
}

/**
* 为当前租户更新或创建记录
*/
public static function updateOrCreateForCurrentTenant(array $attributes, array $values = [])
{
if (app()->has('tenant')) {
$attributes['tenant_id'] = app()->get('tenant')->id;
}
return static::updateOrCreate($attributes, $values);
}
}

优化的 Tenant 模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
// app/Models/Tenant.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
use Carbon\Carbon;

class Tenant extends Model
{
protected $fillable = [
'name',
'slug',
'domain',
'subdomain',
'database_name',
'database_username',
'database_password',
'database_host',
'status',
'trial_ends_at',
'subscription_ends_at',
'billing_email',
'contact_name',
'plan_id',
];

protected $casts = [
'trial_ends_at' => 'datetime',
'subscription_ends_at' => 'datetime',
'status' => 'string',
];

// 缓存键前缀
const CACHE_PREFIX = 'tenant:';

/**
* 租户用户关联
*/
public function users()
{
return $this->hasMany(User::class)->withTimestamps();
}

/**
* 租户订阅关联
*/
public function subscription()
{
return $this->hasOne(Subscription::class)->latest();
}

/**
* 租户计划关联
*/
public function plan()
{
return $this->belongsTo(Plan::class);
}

/**
* 检查租户是否活跃
*/
public function isActive()
{
return Cache::remember(self::CACHE_PREFIX . $this->id . ':active', 60, function () {
return $this->status === 'active' &&
($this->subscription_ends_at === null || $this->subscription_ends_at > Carbon::now());
});
}

/**
* 检查租户是否在试用期
*/
public function isOnTrial()
{
return Cache::remember(self::CACHE_PREFIX . $this->id . ':trial', 60, function () {
return $this->status === 'trial' &&
$this->trial_ends_at && $this->trial_ends_at > Carbon::now();
});
}

/**
* 检查租户是否过期
*/
public function isExpired()
{
return Cache::remember(self::CACHE_PREFIX . $this->id . ':expired', 60, function () {
return $this->status === 'expired' ||
($this->subscription_ends_at && $this->subscription_ends_at < Carbon::now());
});
}

/**
* 获取租户配置
*/
public function getConfig($key = null, $default = null)
{
$cacheKey = self::CACHE_PREFIX . $this->id . ':config';
$config = Cache::remember($cacheKey, 3600, function () {
return $this->config ?? [];
});

return $key ? ($config[$key] ?? $default) : $config;
}

/**
* 更新租户状态
*/
public function updateStatus($status)
{
$this->status = $status;
$this->save();

// 清除缓存
$this->clearCache();
return $this;
}

/**
* 清除租户缓存
*/
public function clearCache()
{
Cache::forget(self::CACHE_PREFIX . $this->id . ':active');
Cache::forget(self::CACHE_PREFIX . $this->id . ':trial');
Cache::forget(self::CACHE_PREFIX . $this->id . ':expired');
Cache::forget(self::CACHE_PREFIX . $this->id . ':config');

// 清除域名缓存
if ($this->domain) {
Cache::forget(self::CACHE_PREFIX . 'domain:' . $this->domain);
}
if ($this->subdomain) {
Cache::forget(self::CACHE_PREFIX . 'subdomain:' . $this->subdomain);
}
}
}

3.4 多租户路由与中间件系统

路由优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// routes/tenant.php

// 租户路由组
Route::middleware(['identify.tenant', 'tenant.active', 'tenant.rate.limit'])->group(function () {
// 租户仪表盘
Route::get('/dashboard', 'Tenant\DashboardController@index')->name('tenant.dashboard');

// 租户设置
Route::prefix('settings')->group(function () {
Route::get('/', 'Tenant\SettingsController@index')->name('tenant.settings');
Route::put('/profile', 'Tenant\SettingsController@updateProfile')->name('tenant.settings.profile');
Route::put('/billing', 'Tenant\SettingsController@updateBilling')->name('tenant.settings.billing');
});

// 租户用户管理
Route::resource('users', 'Tenant\UserController')->names([
'index' => 'tenant.users.index',
'create' => 'tenant.users.create',
'store' => 'tenant.users.store',
'edit' => 'tenant.users.edit',
'update' => 'tenant.users.update',
'destroy' => 'tenant.users.destroy',
]);

// 租户业务路由
Route::resource('projects', 'Tenant\ProjectController');
Route::resource('tasks', 'Tenant\TaskController');
});

租户速率限制中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// app/Http/Middleware/TenantRateLimit.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Cache\RateLimiter;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class TenantRateLimit
{
protected $limiter;

public function __construct(RateLimiter $limiter)
{
$this->limiter = $limiter;
}

public function handle(Request $request, Closure $next, $maxAttempts = 60, $decayMinutes = 1)
{
if (!app()->has('tenant')) {
return $next($request);
}

$tenantId = app()->get('tenant')->id;
$key = 'tenant:' . $tenantId . ':' . $request->ip();

if ($this->limiter->tooManyAttempts($key, $maxAttempts)) {
return response()->json([
'error' => 'Rate limit exceeded. Please try again later.',
'retry_after' => $this->limiter->availableIn($key),
], 429);
}

$this->limiter->hit($key, $decayMinutes * 60);

$response = $next($request);

// 添加速率限制响应头
$response->headers->add([
'X-RateLimit-Limit' => $maxAttempts,
'X-RateLimit-Remaining' => $this->limiter->retriesLeft($key, $maxAttempts),
'X-RateLimit-Reset' => time() + $this->limiter->availableIn($key),
]);

return $response;
}
}

4. Laravel 12 订阅管理系统

4.1 高级订阅管理架构

订阅管理核心组件

组件职责实现方式
计划管理定义产品定价和功能Plan 模型 + 配置系统
订阅生命周期管理订阅状态变化状态机 + 事件系统
支付处理处理支付和退款Cashier + 多支付网关
发票管理生成和发送发票发票模型 + PDF 生成
税务处理计算和处理税费税务服务 + 合规集成
订阅分析分析订阅数据数据仓库 + 报表系统

4.2 Cashier 高级配置与集成

安装与配置

1
2
3
4
5
6
7
8
9
# 安装 Cashier (Stripe 版本)
composer require laravel/cashier

# 发布配置和迁移
php artisan vendor:publish --tag="cashier-migrations"
php artisan vendor:publish --tag="cashier-config"

# 运行迁移
php artisan migrate

高级配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// config/cashier.php
return [
/*
|--------------------------------------------------------------------------
| Cashier 配置
|--------------------------------------------------------------------------
*/
'stripe' => [
'model' => App\Models\Tenant::class, // 使用租户模型
'key' => env('STRIPE_KEY'),
'secret' => env('STRIPE_SECRET'),
'webhook' => [
'secret' => env('STRIPE_WEBHOOK_SECRET'),
'tolerance' => env('STRIPE_WEBHOOK_TOLERANCE', 300),
],
'api_version' => env('STRIPE_API_VERSION', '2024-06-20'),
'maximum_refresh_attempts' => 3,
],

/*
|--------------------------------------------------------------------------
| 发票配置
|--------------------------------------------------------------------------
*/
'invoices' => [
'prefix' => env('INVOICE_PREFIX', 'INV-'),
'number_format' => env('INVOICE_NUMBER_FORMAT', 'Y-m-d-{number}'),
'due_days' => env('INVOICE_DUE_DAYS', 30),
'send_automatically' => env('INVOICE_SEND_AUTOMATICALLY', true),
],

/*
|--------------------------------------------------------------------------
| 税务配置
|--------------------------------------------------------------------------
*/
'tax' => [
'enabled' => env('TAX_ENABLED', true),
'default_rate' => env('TAX_DEFAULT_RATE', 0),
'calculate_tax_automatically' => env('TAX_CALCULATE_AUTOMATICALLY', true),
],
];

租户模型集成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// app/Models/Tenant.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Laravel\Cashier\Billable;

class Tenant extends Model
{
use Billable;

// 其他代码...

/**
* 获取 Stripe 客户 ID
*/
public function getStripeCustomerId()
{
if (! $this->stripe_id) {
$customer = $this->createAsStripeCustomer();
$this->stripe_id = $customer->id;
$this->save();
}
return $this->stripe_id;
}

/**
* 同步订阅状态
*/
public function syncSubscriptionStatus()
{
$subscription = $this->subscription('default');
if ($subscription) {
$stripeSubscription = $subscription->asStripeSubscription();
// 同步 Stripe 订阅状态到本地
$this->updateSubscriptionStatus($stripeSubscription->status);
}
}
}

4.3 多支付网关集成

支付网关抽象层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// app/Services/Payment/PaymentGateway.php
namespace App\Services\Payment;

interface PaymentGateway
{
/**
* 创建订阅
*/
public function createSubscription($customer, $plan, $paymentMethod);

/**
* 取消订阅
*/
public function cancelSubscription($customer, $subscriptionId);

/**
* 恢复订阅
*/
public function resumeSubscription($customer, $subscriptionId);

/**
* 更新支付方式
*/
public function updatePaymentMethod($customer, $paymentMethod);

/**
* 处理退款
*/
public function refund($chargeId, $amount = null);

/**
* 获取交易历史
*/
public function getTransactions($customer, $limit = 10);
}

Stripe 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// app/Services/Payment/StripeGateway.php
namespace App\Services\Payment;

use Stripe\StripeClient;
use App\Models\Tenant;

class StripeGateway implements PaymentGateway
{
protected $stripe;

public function __construct()
{
$this->stripe = new StripeClient(config('cashier.stripe.secret'));
}

public function createSubscription($customer, $plan, $paymentMethod)
{
// 实现 Stripe 订阅创建
}

// 其他方法实现...
}

PayPal 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// app/Services/Payment/PayPalGateway.php
namespace App\Services\Payment;

use PayPalCheckoutSdk\Core\PayPalHttpClient;
use PayPalCheckoutSdk\Core\SandboxEnvironment;
use App\Models\Tenant;

class PayPalGateway implements PaymentGateway
{
protected $client;

public function __construct()
{
$environment = new SandboxEnvironment(
env('PAYPAL_CLIENT_ID'),
env('PAYPAL_CLIENT_SECRET')
);
$this->client = new PayPalHttpClient($environment);
}

public function createSubscription($customer, $plan, $paymentMethod)
{
// 实现 PayPal 订阅创建
}

// 其他方法实现...
}

支付网关工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// app/Services/Payment/PaymentGatewayFactory.php
namespace App\Services\Payment;

class PaymentGatewayFactory
{
/**
* 创建支付网关实例
*/
public static function create($gateway = 'stripe')
{
switch ($gateway) {
case 'stripe':
return new StripeGateway();
case 'paypal':
return new PayPalGateway();
case 'paddle':
return new PaddleGateway();
default:
throw new \InvalidArgumentException("Unsupported payment gateway: {$gateway}");
}
}
}

4.4 高级订阅管理服务

订阅服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
// app/Services/SubscriptionService.php
namespace App\Services;

use App\Models\Tenant;
use App\Models\Plan;
use App\Models\Subscription;
use App\Services\Payment\PaymentGatewayFactory;
use Carbon\Carbon;

class SubscriptionService
{
/**
* 创建订阅
*/
public function createSubscription(Tenant $tenant, Plan $plan, $paymentMethod, $gateway = 'stripe')
{
try {
// 验证租户状态
if (!$tenant->isActive() && !$tenant->isOnTrial()) {
throw new \Exception('Cannot create subscription for inactive tenant');
}

// 验证支付方式
$this->validatePaymentMethod($paymentMethod);

// 获取支付网关
$paymentGateway = PaymentGatewayFactory::create($gateway);

// 创建订阅
$subscription = $paymentGateway->createSubscription($tenant, $plan, $paymentMethod);

// 计算订阅结束时间
$endsAt = $this->calculateSubscriptionEndDate($plan->interval, $plan->interval_count ?? 1);

// 保存本地订阅记录
$localSubscription = Subscription::create([
'tenant_id' => $tenant->id,
'plan_id' => $plan->id,
'status' => 'active',
'starts_at' => Carbon::now(),
'ends_at' => $endsAt,
'payment_method' => $gateway,
'payment_reference' => $subscription->id,
'amount' => $plan->price,
'currency' => $plan->currency,
]);

// 更新租户状态
$tenant->update([
'status' => 'active',
'subscription_ends_at' => $localSubscription->ends_at,
'plan_id' => $plan->id,
]);

// 触发订阅创建事件
event(new \App\Events\SubscriptionCreated($localSubscription));

\Illuminate\Support\Facades\Log::info('Subscription created successfully', [
'tenant_id' => $tenant->id,
'plan_id' => $plan->id,
'subscription_id' => $localSubscription->id,
'gateway' => $gateway,
]);

return $localSubscription;
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Failed to create subscription', [
'tenant_id' => $tenant->id,
'plan_id' => $plan->id,
'gateway' => $gateway,
'error' => $e->getMessage(),
]);

throw $e;
}
}

/**
* 取消订阅
*/
public function cancelSubscription(Subscription $subscription, $immediately = false)
{
try {
// 获取租户和支付网关
$tenant = $subscription->tenant;
$paymentGateway = PaymentGatewayFactory::create($subscription->payment_method);

// 取消支付网关订阅
$paymentGateway->cancelSubscription($tenant, $subscription->payment_reference, $immediately);

// 更新本地订阅状态
$updateData = [
'status' => 'cancelled',
'cancelled_at' => Carbon::now(),
'cancelled_immediately' => $immediately,
];

if ($immediately) {
$updateData['ends_at'] = Carbon::now();
$tenant->update(['status' => 'cancelled']);
}

$subscription->update($updateData);

// 触发订阅取消事件
event(new \App\Events\SubscriptionCancelled($subscription));

\Illuminate\Support\Facades\Log::info('Subscription cancelled successfully', [
'subscription_id' => $subscription->id,
'tenant_id' => $tenant->id,
'immediately' => $immediately,
]);

return $subscription;
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Failed to cancel subscription', [
'subscription_id' => $subscription->id,
'tenant_id' => $tenant->id,
'error' => $e->getMessage(),
]);

throw $e;
}
}

/**
* 恢复订阅
*/
public function resumeSubscription(Subscription $subscription, $paymentMethod = null)
{
try {
// 获取租户和支付网关
$tenant = $subscription->tenant;
$paymentGateway = PaymentGatewayFactory::create($subscription->payment_method);

// 恢复订阅
$resumedSubscription = $paymentGateway->resumeSubscription(
$tenant,
$subscription->payment_reference,
$paymentMethod
);

// 更新本地订阅状态
$endsAt = $this->calculateSubscriptionEndDate(
$subscription->plan->interval,
$subscription->plan->interval_count ?? 1
);

$subscription->update([
'status' => 'active',
'ends_at' => $endsAt,
'resumed_at' => Carbon::now(),
]);

// 更新租户状态
$tenant->update([
'status' => 'active',
'subscription_ends_at' => $endsAt,
]);

// 触发订阅恢复事件
event(new \App\Events\SubscriptionResumed($subscription));

\Illuminate\Support\Facades\Log::info('Subscription resumed successfully', [
'subscription_id' => $subscription->id,
'tenant_id' => $tenant->id,
]);

return $subscription;
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Failed to resume subscription', [
'subscription_id' => $subscription->id,
'tenant_id' => $tenant->id,
'error' => $e->getMessage(),
]);

throw $e;
}
}

/**
* 升级或降级订阅计划
*/
public function changePlan(Subscription $subscription, Plan $newPlan)
{
try {
// 获取租户和支付网关
$tenant = $subscription->tenant;
$paymentGateway = PaymentGatewayFactory::create($subscription->payment_method);

// 计算 prorated 金额
$proration = $this->calculateProration($subscription, $newPlan);

// 更新支付网关订阅
$updatedSubscription = $paymentGateway->updateSubscriptionPlan(
$tenant,
$subscription->payment_reference,
$newPlan,
$proration
);

// 更新本地订阅记录
$endsAt = $this->calculateSubscriptionEndDate(
$newPlan->interval,
$newPlan->interval_count ?? 1
);

$subscription->update([
'plan_id' => $newPlan->id,
'amount' => $newPlan->price,
'currency' => $newPlan->currency,
'ends_at' => $endsAt,
'prorated_amount' => $proration,
'updated_at' => Carbon::now(),
]);

// 更新租户计划
$tenant->update([
'plan_id' => $newPlan->id,
'subscription_ends_at' => $endsAt,
]);

// 触发订阅计划变更事件
event(new \App\Events\SubscriptionPlanChanged($subscription, $newPlan));

\Illuminate\Support\Facades\Log::info('Subscription plan changed successfully', [
'subscription_id' => $subscription->id,
'tenant_id' => $tenant->id,
'old_plan_id' => $subscription->plan_id,
'new_plan_id' => $newPlan->id,
]);

return $subscription;
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Failed to change subscription plan', [
'subscription_id' => $subscription->id,
'tenant_id' => $tenant->id,
'new_plan_id' => $newPlan->id,
'error' => $e->getMessage(),
]);

throw $e;
}
}

/**
* 处理订阅过期
*/
public function handleSubscriptionExpiry()
{
try {
$expiringSubscriptions = Subscription::where('status', 'active')
->where('ends_at', '<=', Carbon::now())
->get();

foreach ($expiringSubscriptions as $subscription) {
$this->expireSubscription($subscription);
}

\Illuminate\Support\Facades\Log::info('Subscription expiry handling completed', [
'expired_count' => $expiringSubscriptions->count(),
]);

return $expiringSubscriptions->count();
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Failed to handle subscription expiry', [
'error' => $e->getMessage(),
]);

return 0;
}
}

/**
* 过期订阅
*/
private function expireSubscription(Subscription $subscription)
{
try {
// 更新订阅状态
$subscription->update([
'status' => 'expired',
'expired_at' => Carbon::now(),
]);

// 更新租户状态
$tenant = $subscription->tenant;
$tenant->update([
'status' => 'expired',
]);

// 触发订阅过期事件
event(new \App\Events\SubscriptionExpired($subscription));

\Illuminate\Support\Facades\Log::info('Subscription expired', [
'subscription_id' => $subscription->id,
'tenant_id' => $tenant->id,
]);
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Failed to expire subscription', [
'subscription_id' => $subscription->id,
'error' => $e->getMessage(),
]);
}
}

/**
* 计算订阅结束日期
*/
private function calculateSubscriptionEndDate($interval, $intervalCount = 1)
{
$now = Carbon::now();

switch ($interval) {
case 'day':
return $now->addDays($intervalCount);
case 'week':
return $now->addWeeks($intervalCount);
case 'month':
return $now->addMonths($intervalCount);
case 'year':
return $now->addYears($intervalCount);
case 'daily':
return $now->addDays($intervalCount);
case 'weekly':
return $now->addWeeks($intervalCount);
case 'monthly':
return $now->addMonths($intervalCount);
case 'yearly':
return $now->addYears($intervalCount);
default:
return $now->addMonths(1);
}
}

/**
* 验证支付方式
*/
private function validatePaymentMethod($paymentMethod)
{
if (empty($paymentMethod)) {
throw new \Exception('Payment method is required');
}

// 实现支付方式验证逻辑
// ...
}

/**
* 计算 prorated 金额
*/
private function calculateProration(Subscription $subscription, Plan $newPlan)
{
// 计算剩余订阅天数
$daysRemaining = $subscription->ends_at->diffInDays(Carbon::now());
$totalDays = $subscription->ends_at->diffInDays($subscription->starts_at);

// 计算剩余价值
$remainingValue = ($subscription->amount / $totalDays) * $daysRemaining;

// 计算新计划的日价值
$newPlanDailyValue = $newPlan->price / $this->getPlanDays($newPlan->interval);

// 计算 prorated 金额
$proration = $remainingValue - ($newPlanDailyValue * $daysRemaining);

return round($proration, 2);
}

/**
* 获取计划天数
*/
private function getPlanDays($interval)
{
switch ($interval) {
case 'month':
case 'monthly':
return 30;
case 'year':
case 'yearly':
return 365;
case 'week':
case 'weekly':
return 7;
case 'day':
case 'daily':
return 1;
default:
return 30;
}
}
}

4.5 发票管理与税务处理

发票模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// app/Models/Invoice.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;

class Invoice extends Model
{
protected $fillable = [
'tenant_id',
'subscription_id',
'invoice_number',
'amount',
'tax_amount',
'total_amount',
'status', // draft, issued, paid, overdue, cancelled
'issue_date',
'due_date',
'payment_date',
'pdf_path',
'stripe_invoice_id',
];

protected $dates = [
'issue_date',
'due_date',
'payment_date',
];

/**
* 租户关联
*/
public function tenant()
{
return $this->belongsTo(Tenant::class);
}

/**
* 订阅关联
*/
public function subscription()
{
return $this->belongsTo(Subscription::class);
}

/**
* 生成发票号码
*/
public static function generateInvoiceNumber()
{
$prefix = config('cashier.invoices.prefix', 'INV-');
$format = config('cashier.invoices.number_format', 'Y-m-d-{number}');

$lastInvoice = self::latest('id')->first();
$number = $lastInvoice ? $lastInvoice->id + 1 : 1;

return str_replace('{number}', $number, $format);
}

/**
* 生成 PDF 发票
*/
public function generatePdf()
{
// 使用 PDF 生成库生成发票
// 例如:barryvdh/laravel-dompdf
$pdf = app('dompdf.wrapper');
$pdf->loadView('invoices.pdf', ['invoice' => $this]);

// 保存 PDF
$path = storage_path('app/invoices/' . $this->invoice_number . '.pdf');
$pdf->save($path);

// 更新 PDF 路径
$this->update(['pdf_path' => $path]);

return $path;
}
}

税务服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// app/Services/TaxService.php
namespace App\Services;

use App\Models\Tenant;
use App\Models\Plan;

class TaxService
{
/**
* 计算税费
*/
public function calculateTax(Tenant $tenant, Plan $plan)
{
if (! config('cashier.tax.enabled')) {
return 0;
}

// 获取租户税务信息
$taxInfo = $tenant->tax_info ?? [];

// 根据租户位置计算税费
$taxRate = $this->getTaxRate($taxInfo);

// 计算税费
$taxAmount = $plan->price * ($taxRate / 100);

return round($taxAmount, 2);
}

/**
* 获取税率
*/
private function getTaxRate($taxInfo)
{
// 实现税率计算逻辑
// 例如:基于国家/地区的税率表
return config('cashier.tax.default_rate', 0);
}

/**
* 验证税务信息
*/
public function validateTaxInfo($taxInfo)
{
// 实现税务信息验证逻辑
return true;
}
}

5. Laravel 12 SaaS 技术生态系统

5.1 核心框架性能基准测试

Laravel 12 性能对比

配置请求/秒平均响应时间 (ms)99% 响应时间 (ms)内存使用 (MB)
Laravel 11350-45022-3080-100120-150
Laravel 12 (默认)450-55018-2560-80100-130
Laravel 12 + Octane1500-20004-815-2580-100
Laravel 12 + FrankenPHP2000-25003-610-2070-90

最佳实践配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// config/octane.php
return [
'server' => env('OCTANE_SERVER', 'roadrunner'),
'watch' => [
'paths' => [
base_path('app'),
base_path('config'),
base_path('routes'),
base_path('resources/views'),
],
],
'warm' => [
'routes' => true,
'views' => true,
'cache' => true,
],
];

5.2 多租户框架深度分析

框架对比

框架架构模式性能功能完整性维护状态适用场景
Stancl/Multitenancy灵活配置活跃中小规模 SaaS
Tenancy/Framework完整解决方案极高活跃企业级 SaaS
Hyn/Multi-tenant传统架构维护中小型 SaaS
Spatie/Multitenancy轻量级活跃初创项目

Stancl/Multitenancy 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// config/multitenancy.php
return [
'tenant_model' => App\Models\Tenant::class,
'id_column' => 'id',
'domain_column' => 'domain',
'prefix_base' => 'tenant_',
'database' => [
'tenant_connection_name' => 'tenant',
'central_connection_name' => 'mysql',
],
'cache' => [
'store' => 'redis',
'key_prefix' => 'tenant:',
],
];

5.3 支付处理生态系统

集成架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌─────────────────────────────────────────┐
│ 应用层 │
├─────────────┬─────────────┬─────────────┤
│ 订阅管理 │ 发票管理 │ 税务处理 │
└─────────────┴─────────────┴─────────────┘

┌─────────────────────────────────────────┐
│ 支付网关层 │
├─────────────┬─────────────┬─────────────┤
│ Stripe │ PayPal │ 其他网关 │
└─────────────┴─────────────┴─────────────┘

┌─────────────────────────────────────────┐
│ 银行/支付网络 │
└─────────────────────────────────────────┘

推荐工具链

场景推荐工具优势
订阅计费Laravel Cashier + Stripe完整的订阅管理
一次性支付Srmklive/PayPal简单集成 PayPal
多网关支持Omnipay/omnipay统一的支付接口
欧洲支付Mollie/laravel-mollie支持本地支付方式
加密货币Coinbase Commerce支持加密货币支付

5.4 前端架构最佳实践

技术栈选择

应用类型推荐前端技术性能开发效率SEO
管理后台Livewire + Alpine.js极高
客户门户Inertia.js + Vue 3
营销网站Blade + Tailwind CSS极高
单页应用React + Next.js极高

前端优化策略

  1. 代码分割:按路由或组件分割代码
  2. 资源压缩:使用 Vite 进行代码压缩
  3. 缓存策略:合理设置 HTTP 缓存头
  4. CDN 集成:使用 CDN 加速静态资源
  5. 预加载:关键资源预加载
  6. 服务端渲染:提高首屏加载速度

5.5 部署和监控生态系统

企业级部署架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌──────────────────────────────────────────────────────┐
│ 全球负载均衡 │
├─────────────┬─────────────┬─────────────┬─────────────┤
│ 区域 A │ 区域 B │ 区域 C │ 区域 D │
└─────────────┴─────────────┴─────────────┴─────────────┘

┌──────────────────────────────────────────────────────┐
│ Kubernetes 集群 │
├─────────────┬─────────────┬─────────────┬─────────────┤
│ API 网关 │ 应用服务 │ 队列服务 │ 后台服务 │
└─────────────┴─────────────┴─────────────┴─────────────┘

┌──────────────────────────────────────────────────────┐
│ 数据层 │
├─────────────┬─────────────┬─────────────┬─────────────┤
│ 主数据库 │ 只读副本 │ Redis 集群 │ 对象存储 │
└─────────────┴─────────────┴─────────────┴─────────────┘

┌──────────────────────────────────────────────────────┐
│ 监控告警 │
├─────────────┬─────────────┬─────────────┬─────────────┤
│ Prometheus │ Grafana │ Alertmanager │ ELK Stack │
└─────────────┴─────────────┴─────────────┴─────────────┘

监控配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# prometheus.yml
scrape_configs:
- job_name: 'laravel'
static_configs:
- targets: ['app:9100']
metrics_path: '/metrics'
scrape_interval: 15s

- job_name: 'mysql'
static_configs:
- targets: ['mysql:9104']

- job_name: 'redis'
static_configs:
- targets: ['redis:9121']

告警规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# alertmanager.yml
route:
group_by: ['alertname', 'cluster', 'service']
receiver: 'email'

receivers:
- name: 'email'
email_configs:
- to: 'alerts@example.com'
send_resolved: true

alerts:
- alert: HighCPUUsage
expr: avg(node_cpu_seconds_total{mode="idle"}) by (instance) < 0.2
for: 5m
labels:
severity: critical
annotations:
summary: "High CPU usage on {{ $labels.instance }}"
description: "CPU usage has been above 80% for 5 minutes"

5.6 开发工具链优化

CI/CD 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# .github/workflows/ci.yml
name: CI

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: [8.2, 8.3]
laravel: [12.*]

steps:
- uses: actions/checkout@v3

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: mbstring, dom, curl, sqlite3, redis, mysql
coverage: xdebug

- name: Install dependencies
run: composer install --prefer-dist --no-progress

- name: Run tests
run: vendor/bin/phpunit --coverage-clover=coverage.xml

- name: Code quality
run: vendor/bin/phpstan analyse

- name: Security scan
run: vendor/bin/psalm

deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'

steps:
- uses: actions/checkout@v3

- name: Deploy to production
run: |
# 部署脚本
ssh deploy@example.com "cd /var/www/app && git pull && composer install && php artisan migrate --force"

开发环境配置

1
2
3
4
5
6
7
8
9
# 使用 Laravel Sail
composer require laravel/sail --dev
php artisan sail:install

# 启动开发环境
./vendor/bin/sail up

# 安装开发工具
./vendor/bin/sail composer require --dev \n barryvdh/laravel-debugbar \n barryvdh/laravel-ide-helper \n beyondcode/laravel-dump-server \n fakerphp/faker \n mockery/mockery \n nunomaduro/collision \n nunomaduro/larastan \n phpunit/phpunit \n spatie/laravel-ignition

6. Laravel 12 多租户最佳实践

6.1 数据隔离与安全

多层次数据隔离策略

  1. 数据库层隔离

    • 使用数据库视图限制租户数据访问
    • 实现行级安全策略(RLS)
    • 定期审计数据访问日志
  2. 应用层隔离

    • 实现 TenantGuard 确保租户用户只能访问自己的数据
    • 使用 Policy 系统细化租户内权限控制
    • 实现租户数据导出和导入的安全机制
  3. 存储层隔离

    • 为每个租户创建独立的文件存储目录
    • 使用 S3 对象存储的前缀或桶隔离
    • 实现租户文件的加密存储

安全增强实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// app/Http/Middleware/TenantSecurity.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Gate;

class TenantSecurity
{
public function handle($request, Closure $next)
{
// 验证租户访问权限
$this->validateTenantAccess($request);

// 注册租户特定的权限
$this->registerTenantPolicies();

// 应用租户安全头
$response = $next($request);
$this->applySecurityHeaders($response);

return $response;
}

/**
* 验证租户访问权限
*/
private function validateTenantAccess($request)
{
$tenant = $request->tenant;
$user = $request->user();

// 确保用户属于当前租户
if ($user && $user->tenant_id !== $tenant->id) {
abort(403, 'Unauthorized access to tenant');
}

// 验证租户状态
if (!$tenant->isActive()) {
abort(403, 'Tenant account is inactive');
}
}

/**
* 注册租户特定的权限
*/
private function registerTenantPolicies()
{
// 动态注册租户特定的策略
Gate::define('manage-tenant', function ($user, $tenant) {
return $user->tenant_id === $tenant->id && $user->hasRole('admin');
});
}

/**
* 应用安全头
*/
private function applySecurityHeaders($response)
{
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('X-XSS-Protection', '1; mode=block');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
}
}

6.2 性能优化策略

数据库优化

  1. 索引优化

    • tenant_id 字段创建复合索引
    • 使用覆盖索引减少查询开销
    • 定期重建索引保持性能
  2. 查询优化

    • 实现租户级查询缓存
    • 使用 select 方法指定必要字段
    • 避免使用 * 查询和 N+1 问题
  3. 连接池管理

    • 实现基于租户的连接池
    • 配置合理的连接超时和最大连接数
    • 使用连接池监控确保资源合理使用

缓存策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// app/Services/CacheService.php
namespace App\Services;

use Illuminate\Support\Facades\Cache;

class CacheService
{
/**
* 获取租户缓存键
*/
public function getTenantCacheKey($key, $tenantId = null)
{
$tenantId = $tenantId ?? app()->get('tenant')->id;
return "tenant:{$tenantId}:{$key}";
}

/**
* 缓存租户数据
*/
public function cacheTenantData($key, $data, $ttl = 3600, $tenantId = null)
{
$cacheKey = $this->getTenantCacheKey($key, $tenantId);
return Cache::put($cacheKey, $data, $ttl);
}

/**
* 获取租户缓存数据
*/
public function getTenantData($key, $default = null, $tenantId = null)
{
$cacheKey = $this->getTenantCacheKey($key, $tenantId);
return Cache::get($cacheKey, $default);
}

/**
* 清除租户缓存
*/
public function clearTenantCache($pattern = '*', $tenantId = null)
{
$tenantId = $tenantId ?? app()->get('tenant')->id;
$pattern = "tenant:{$tenantId}:{$pattern}";

// 清除匹配的缓存键
$keys = Cache::keys($pattern);
foreach ($keys as $key) {
Cache::forget($key);
}
}
}

队列优化

  • 为每个租户创建独立的队列
  • 实现队列优先级基于租户订阅级别
  • 使用 Horizon 监控队列性能

6.3 可扩展性设计

模块化架构

  1. 服务导向设计

    • 将业务逻辑拆分为独立服务
    • 实现服务接口和依赖注入
    • 使用事件系统解耦服务间通信
  2. 插件系统

    • 实现插件注册和发现机制
    • 提供插件 API 和钩子系统
    • 支持插件的版本管理和依赖解析
  3. 功能开关系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// app/Services/FeatureService.php
namespace App\Services;

use App\Models\Tenant;
use Illuminate\Support\Facades\Cache;

class FeatureService
{
/**
* 检查租户是否启用了某个功能
*/
public function isFeatureEnabled($feature, Tenant $tenant = null)
{
$tenant = $tenant ?? app()->get('tenant');

// 获取租户计划的功能列表
$features = $this->getPlanFeatures($tenant->plan_id);

// 检查功能是否在计划中
if (!in_array($feature, $features)) {
return false;
}

// 检查租户是否有特定的功能覆盖
$overrides = $tenant->feature_overrides ?? [];
return $overrides[$feature] ?? true;
}

/**
* 获取计划的功能列表
*/
public function getPlanFeatures($planId)
{
return Cache::remember("plan:{$planId}:features", 3600, function () use ($planId) {
$plan = \App\Models\Plan::find($planId);
return $plan ? $plan->features : [];
});
}

/**
* 为租户启用或禁用功能
*/
public function toggleFeature($feature, $enabled, Tenant $tenant = null)
{
$tenant = $tenant ?? app()->get('tenant');

$overrides = $tenant->feature_overrides ?? [];
$overrides[$feature] = $enabled;

$tenant->update(['feature_overrides' => $overrides]);

// 清除缓存
Cache::forget("tenant:{$tenant->id}:features");

return $enabled;
}
}

6.4 监控与可观测性

租户级监控

  1. 性能监控

    • 实现租户级请求速率和响应时间监控
    • 跟踪租户数据库查询性能
    • 监控租户缓存命中率
  2. 使用监控

    • 跟踪租户 API 调用频率和模式
    • 监控租户存储使用情况
    • 分析租户功能使用统计
  3. 告警系统

    • 为异常租户行为设置告警
    • 监控租户订阅状态变化
    • 实现自动扩缩容基于租户负载

监控仪表盘实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// app/Http/Controllers/Tenant/DashboardController.php
namespace App\Http\Controllers\Tenant;

use App\Services\MonitoringService;
use Illuminate\Http\Request;

class DashboardController extends Controller
{
public function index(Request $request, MonitoringService $monitoring)
{
$tenant = $request->tenant;

// 获取租户性能指标
$metrics = $monitoring->getTenantMetrics($tenant->id);

// 获取租户使用统计
$usage = $monitoring->getTenantUsage($tenant->id);

// 获取告警信息
$alerts = $monitoring->getTenantAlerts($tenant->id);

return view('tenant.dashboard', compact('metrics', 'usage', 'alerts'));
}
}

7. Laravel 12 SaaS 部署策略

7.1 云服务架构优化

多区域部署架构

组件区域 A (主)区域 B (备)区域 C (边缘)
应用服务器多实例,负载均衡多实例,热备份轻量级边缘节点
数据库主库 + 只读副本异步复制副本
缓存Redis 集群Redis 哨兵本地缓存
存储S3 主桶S3 跨区域复制边缘缓存
CDN全局分发全局分发本地节点

云服务选择矩阵

业务需求推荐云服务配置建议
全球覆盖AWS Global多区域部署,Route 53 路由
企业合规Azure Government符合 FedRAMP 标准
成本优化DigitalOcean + Cloudflare混合云架构
AI 集成Google Cloud利用 Vertex AI 服务
中国区服务阿里云 + 腾讯云合规部署

7.2 容器化部署最佳实践

多阶段 Docker 构建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# Dockerfile
FROM php:8.3-fpm-alpine AS base

# 安装依赖
RUN apk add --no-cache \
libpng-dev \
libjpeg-turbo-dev \
freetype-dev \
libzip-dev \
curl \
git \
bash \
nginx \
supervisor

# 安装 PHP 扩展
RUN docker-php-ext-install \
pdo_mysql \
mysqli \
gd \
zip \
bcmath \
opcache

# 配置 PHP
COPY docker/php.ini /usr/local/etc/php/conf.d/custom.ini

# 配置 Nginx
COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY docker/default.conf /etc/nginx/conf.d/default.conf

# 配置 Supervisor
COPY docker/supervisord.conf /etc/supervisord.conf

FROM base AS builder

# 安装 Composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

# 设置工作目录
WORKDIR /var/www/html

# 复制依赖文件
COPY composer.json composer.lock ./

# 安装依赖
RUN composer install --no-dev --optimize-autoloader --no-progress

FROM base AS production

# 复制构建结果
COPY --from=builder /var/www/html/vendor /var/www/html/vendor

# 复制应用代码
COPY . /var/www/html

# 设置权限
RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache

# 生成应用密钥
RUN php artisan key:generate --ansi

# 优化应用
RUN php artisan config:cache
RUN php artisan route:cache
RUN php artisan view:cache

# 暴露端口
EXPOSE 80

# 启动服务
CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisord.conf"]

Docker Compose 生产配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# docker-compose.prod.yml
version: '3.8'

services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "80:80"
volumes:
- ./storage:/var/www/html/storage
- ./logs:/var/www/html/storage/logs
environment:
- APP_ENV=production
- APP_DEBUG=false
- DB_HOST=db
- DB_PORT=3306
- DB_DATABASE=${DB_DATABASE}
- DB_USERNAME=${DB_USERNAME}
- DB_PASSWORD=${DB_PASSWORD}
- REDIS_HOST=redis
- REDIS_PASSWORD=${REDIS_PASSWORD}
- CACHE_DRIVER=redis
- QUEUE_CONNECTION=redis
- SESSION_DRIVER=redis
- MAIL_MAILER=smtp
- MAIL_HOST=${MAIL_HOST}
- MAIL_PORT=${MAIL_PORT}
- MAIL_USERNAME=${MAIL_USERNAME}
- MAIL_PASSWORD=${MAIL_PASSWORD}
- MAIL_ENCRYPTION=${MAIL_ENCRYPTION}
- MAIL_FROM_ADDRESS=${MAIL_FROM_ADDRESS}
- STRIPE_KEY=${STRIPE_KEY}
- STRIPE_SECRET=${STRIPE_SECRET}
depends_on:
- db
- redis
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 60s

db:
image: mysql:8.0
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
- ./docker/mysql.cnf:/etc/mysql/conf.d/custom.cnf
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_DATABASE=${DB_DATABASE}
- MYSQL_USER=${DB_USERNAME}
- MYSQL_PASSWORD=${DB_PASSWORD}
restart: unless-stopped
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 30s
timeout: 5s
retries: 3

redis:
image: redis:7.0-alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
command: redis-server --requirepass ${REDIS_PASSWORD}
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 5s
retries: 3

horizon:
build:
context: .
dockerfile: Dockerfile
command: php artisan horizon
volumes:
- ./storage:/var/www/html/storage
- ./logs:/var/www/html/storage/logs
environment:
- APP_ENV=production
- APP_DEBUG=false
- DB_HOST=db
- REDIS_HOST=redis
- REDIS_PASSWORD=${REDIS_PASSWORD}
depends_on:
- app
- redis
restart: unless-stopped

scheduler:
build:
context: .
dockerfile: Dockerfile
command: bash -c "while true; do php artisan schedule:run; sleep 60; done"
volumes:
- ./storage:/var/www/html/storage
- ./logs:/var/www/html/storage/logs
environment:
- APP_ENV=production
- APP_DEBUG=false
- DB_HOST=db
- REDIS_HOST=redis
- REDIS_PASSWORD=${REDIS_PASSWORD}
depends_on:
- app
- redis
restart: unless-stopped

volumes:
mysql-data:
redis-data:

7.3 自动扩展策略

Kubernetes 部署配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-saas
namespace: production
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: laravel-saas
template:
metadata:
labels:
app: laravel-saas
spec:
containers:
- name: app
image: your-registry/laravel-saas:latest
ports:
- containerPort: 80
envFrom:
- secretRef:
name: laravel-secrets
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"
readinessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 80
initialDelaySeconds: 60
periodSeconds: 30

---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: laravel-saas-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: laravel-saas
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80

数据库分片策略

  • 范围分片:根据租户 ID 范围分配数据库
  • 哈希分片:使用租户 ID 的哈希值分配数据库
  • 地理位置分片:根据租户地理位置分配数据库

7.4 可观测性系统

全栈监控架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌─────────────────────────────────────────────────┐
│ 监控层 │
├─────────────┬─────────────┬─────────────┬─────────┤
│ Prometheus │ Grafana │ Alertmanager │ Sentry │
└─────────────┴─────────────┴─────────────┴─────────┘

┌─────────────────────────────────────────────────┐
│ 数据采集层 │
├─────────────┬─────────────┬─────────────┬─────────┤
│ Node Exporter │ MySQL Exporter │ Redis Exporter │ App Metrics │
└─────────────┴─────────────┴─────────────┴─────────┘

┌─────────────────────────────────────────────────┐
│ 应用层 │
├─────────────┬─────────────┬─────────────┬─────────┤
│ Laravel App │ Horizon │ Scheduler │ Queue │
└─────────────┴─────────────┴─────────────┴─────────┘

应用指标暴露

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// app/Http/Controllers/MetricsController.php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;

class MetricsController extends Controller
{
public function index(Request $request)
{
// 应用指标
$metrics = [
// 请求计数
'requests_total' => Cache::get('metrics:requests:total', 0),

// 响应时间
'response_time_seconds' => Cache::get('metrics:response:time', 0),

// 数据库连接数
'database_connections' => DB::connection()->getPdo() ? 1 : 0,

// 队列任务数
'queue_jobs_pending' => Queue::size('default'),

// 缓存命中率
'cache_hit_rate' => $this->calculateCacheHitRate(),

// 租户数
'tenants_total' => \App\Models\Tenant::count(),

// 活跃订阅数
'subscriptions_active' => \App\Models\Subscription::where('status', 'active')->count(),
];

// 格式化为 Prometheus 格式
$output = $this->formatForPrometheus($metrics);

return response($output, 200, [
'Content-Type' => 'text/plain',
]);
}

/**
* 计算缓存命中率
*/
private function calculateCacheHitRate()
{
$hits = Cache::get('metrics:cache:hits', 0);
$misses = Cache::get('metrics:cache:misses', 1);
return $hits / ($hits + $misses);
}

/**
* 格式化为 Prometheus 格式
*/
private function formatForPrometheus($metrics)
{
$output = "";
foreach ($metrics as $key => $value) {
$output .= "# HELP laravel_{$key} Laravel {$key}\n";
$output .= "# TYPE laravel_{$key} gauge\n";
$output .= "laravel_{$key} {$value}\n\n";
}
return $output;
}
}

告警规则配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# prometheus/rules/laravel-alerts.yml
groups:
- name: laravel-alerts
rules:
- alert: HighCpuUsage
expr: (100 - (avg by(instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)) > 80
for: 5m
labels:
severity: warning
annotations:
summary: "High CPU usage on {{ $labels.instance }}"
description: "CPU usage has been above 80% for 5 minutes"

- alert: HighMemoryUsage
expr: (node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes * 100 > 85
for: 5m
labels:
severity: warning
annotations:
summary: "High memory usage on {{ $labels.instance }}"
description: "Memory usage has been above 85% for 5 minutes"

- alert: DatabaseDown
expr: mysql_up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Database down on {{ $labels.instance }}"
description: "MySQL instance is not responding"

- alert: RedisDown
expr: redis_up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Redis down on {{ $labels.instance }}"
description: "Redis instance is not responding"

- alert: HighQueueBacklog
expr: laravel_queue_jobs_pending > 100
for: 5m
labels:
severity: warning
annotations:
summary: "High queue backlog"
description: "Queue has more than 100 pending jobs for 5 minutes"

- alert: LowCacheHitRate
expr: laravel_cache_hit_rate < 0.8
for: 10m
labels:
severity: warning
annotations:
summary: "Low cache hit rate"
description: "Cache hit rate has been below 80% for 10 minutes"

8. Laravel 12 SaaS 实战案例:构建 TaskFlow 应用

8.1 Laravel 12 SaaS 项目背景

  • 项目名称:TaskFlow
  • 项目类型:任务管理 SaaS 应用
  • 目标用户:中小企业和团队
  • 核心功能:任务管理、项目管理、团队协作、时间跟踪、报表分析
  • 订阅计划:免费版、基础版、专业版、企业版

8.2 Laravel 12 SaaS 架构设计

1. 技术栈

  • 后端:Laravel 12、PHP 8.2
  • 前端:Vue 3、Tailwind CSS、Inertia.js
  • 数据库:MySQL 8.0
  • 缓存:Redis 7.0
  • 队列:Laravel Horizon、Redis
  • 认证:Laravel Sanctum
  • 支付:Laravel Cashier、Stripe
  • 部署:Docker、Kubernetes、AWS

2. 多租户实现

  • 架构模式:共享数据库,隔离架构
  • 表结构:使用租户 ID 作为外键
  • 数据隔离:通过中间件和查询作用域实现
  • 缓存策略:使用租户 ID 作为缓存键前缀

3. 目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
app/
├── Http/
│ ├── Controllers/
│ │ ├── Tenant/
│ │ │ ├── TaskController.php
│ │ │ ├── ProjectController.php
│ │ │ └── TeamController.php
│ │ └── Admin/
│ │ ├── TenantController.php
│ │ ├── PlanController.php
│ │ └── SubscriptionController.php
│ ├── Middleware/
│ │ ├── IdentifyTenant.php
│ │ └── TenantAccess.php
│ └── Resources/
├── Models/
│ ├── Tenant.php
│ ├── User.php
│ ├── Plan.php
│ ├── Subscription.php
│ ├── Task.php
│ └── Project.php
├── Providers/
│ ├── TenantServiceProvider.php
│ └── AppServiceProvider.php
└── Services/
├── TenantService.php
├── SubscriptionService.php
└── BillingService.php

8.3 Laravel 12 SaaS 核心功能实现

1. 任务管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// app/Models/Task.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Task extends Model
{
protected $fillable = [
'tenant_id',
'project_id',
'user_id',
'title',
'description',
'status',
'priority',
'due_date',
'completed_at',
];

protected static function booted()
{
static::addGlobalScope('tenant', function ($query) {
if (app()->has('tenant')) {
$query->where('tenant_id', app()->get('tenant')->id);
}
});
}

public function tenant()
{
return $this->belongsTo(Tenant::class);
}

public function project()
{
return $this->belongsTo(Project::class);
}

public function user()
{
return $this->belongsTo(User::class);
}
}

2. 订阅管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// app/Services/SubscriptionService.php
namespace App\Services;

use App\Models\Tenant;
use App\Models\Plan;
use App\Models\Subscription;
use Carbon\Carbon;

class SubscriptionService
{
public function createSubscription(Tenant $tenant, Plan $plan, $paymentMethod)
{
// 创建订阅记录
$subscription = Subscription::create([
'tenant_id' => $tenant->id,
'plan_id' => $plan->id,
'status' => 'active',
'starts_at' => Carbon::now(),
'ends_at' => Carbon::now()->addMonth(),
'payment_method' => $paymentMethod,
]);

// 更新租户状态
$tenant->update(['status' => 'active']);

return $subscription;
}

public function cancelSubscription(Subscription $subscription)
{
$subscription->update([
'status' => 'cancelled',
]);

return $subscription;
}

public function renewSubscription(Subscription $subscription)
{
$subscription->update([
'status' => 'active',
'ends_at' => Carbon::now()->addMonth(),
]);

return $subscription;
}
}

8.4 Laravel 12 SaaS 性能优化

  • 数据库索引:为租户 ID、用户 ID 等字段添加索引
  • 查询优化:使用 eager loading 减少 N+1 查询
  • 缓存策略:缓存热点数据,如租户配置和计划信息
  • 队列处理:将邮件发送、报表生成等耗时操作放入队列
  • 水平扩展:使用负载均衡和自动扩缩容

8.5 Laravel 12 SaaS 监控和告警

  • 应用监控:使用 Laravel Telescope 和 Sentry
  • 服务器监控:使用 Prometheus 和 Grafana
  • 数据库监控:监控查询性能和连接数
  • 队列监控:使用 Laravel Horizon
  • 告警机制:设置合理的告警阈值

9. 总结

Laravel 12 是构建 SaaS 应用的理想框架,其优雅的架构、强大的生态系统和丰富的功能使其成为开发者的首选。通过本文的介绍,开发者可以掌握:

  1. SaaS 架构设计:理解单租户和多租户架构的区别和选择
  2. 多租户实现:使用 Laravel 12 实现多租户架构
  3. 订阅管理:使用 Laravel Cashier 管理订阅和支付
  4. 常用框架和工具:掌握 SaaS 开发中常用的框架和工具
  5. 最佳实践:遵循 SaaS 开发的最佳实践
  6. 部署策略:选择合适的部署方案和云服务
  7. 性能优化:提高 SaaS 应用的性能和可靠性
  8. 安全措施:确保租户数据安全和隔离

在实际开发中,开发者应该根据项目的具体需求和规模,选择合适的架构模式和技术栈。同时,应该注重代码质量和测试,确保应用的可靠性和可维护性。

随着云计算和 SaaS 模式的不断发展,Laravel 12 也在不断演进,为开发者提供更多强大的功能和工具。通过持续学习和实践,开发者可以构建出高质量、高性能的 SaaS 应用,满足用户的需求,实现商业价值。