ThinkPHP 6.0多租户SaaS系统架构设计与实现完整指南

2025-10-27 0 587

发布日期:2024年1月16日 | 作者:PHP架构师

一、多租户SaaS系统概念解析

多租户SaaS(Software as a Service)架构是现代云应用的核心设计模式,它允许单个应用实例为多个客户(租户)提供服务,同时确保数据隔离和安全性。ThinkPHP 6.0凭借其强大的中间件、事件系统和数据库抽象层,成为构建此类系统的理想选择。

多租户实现方案对比:

方案类型 数据库分离度 维护成本 适用场景
独立数据库 完全隔离 较高 大型企业级应用
共享数据库独立Schema 逻辑隔离 中等 中型SaaS应用
共享数据库共享Schema 数据行级隔离 较低 初创SaaS项目

二、系统架构设计

1. 目录结构设计

app/
├── controller/
│   ├── BaseController.php      # 基础控制器
│   ├── TenantController.php    # 租户相关控制器
│   └── ...
├── middleware/
│   ├── TenantAuth.php          # 租户认证中间件
│   └── TenantInitialize.php    # 租户初始化中间件
├── model/
│   ├── Tenant.php              # 租户模型
│   ├── trait/
│   │   └── TenantScope.php     # 租户作用域 trait
│   └── ...
├── service/
│   ├── TenantService.php       # 租户服务类
│   └── ...
└── ...

2. 数据库设计

-- 系统租户表
CREATE TABLE `tenants` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(100) NOT NULL COMMENT '租户名称',
  `subdomain` varchar(50) NOT NULL COMMENT '子域名',
  `database_name` varchar(100) DEFAULT NULL COMMENT '独立数据库名',
  `status` tinyint(1) DEFAULT '1' COMMENT '状态:1-正常 0-禁用',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_subdomain` (`subdomain`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 租户独立数据库中的用户表(示例)
CREATE TABLE `tenant_users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `tenant_id` int(11) NOT NULL COMMENT '租户ID',
  `name` varchar(50) NOT NULL,
  `email` varchar(100) NOT NULL,
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

三、核心功能实现

1. 租户识别中间件

<?php
namespace appmiddleware;

class TenantInitialize
{
    public function handle($request, Closure $next)
    {
        // 通过子域名识别租户
        $subdomain = $this->getSubdomain($request->host());
        
        if (!$subdomain) {
            // 默认租户或跳转到主域名
            return redirect('https://main-domain.com');
        }
        
        // 查询租户信息
        $tenant = appmodelTenant::where('subdomain', $subdomain)
            ->where('status', 1)
            ->find();
            
        if (!$tenant) {
            throw new thinkexceptionHttpException(404, '租户不存在');
        }
        
        // 设置当前租户上下文
        appserviceTenantService::setCurrentTenant($tenant);
        
        // 如果是独立数据库模式,切换数据库连接
        if ($tenant->database_name) {
            $this->switchDatabase($tenant->database_name);
        }
        
        return $next($request);
    }
    
    private function getSubdomain($host)
    {
        $mainDomain = 'main-domain.com';
        if (strpos($host, $mainDomain) !== false) {
            return str_replace('.' . $mainDomain, '', $host);
        }
        return null;
    }
    
    private function switchDatabase($databaseName)
    {
        thinkfacadeDb::connect('tenant_' . $databaseName, [
            'type'      => 'mysql',
            'hostname'  => '127.0.0.1',
            'database'  => $databaseName,
            'username'  => 'username',
            'password'  => 'password',
            'charset'   => 'utf8mb4',
        ]);
    }
}

2. 租户作用域 Trait

<?php
namespace appmodeltrait;

trait TenantScope
{
    protected static function bootTenantScope()
    {
        static::addGlobalScope('tenant', function ($builder) {
            $tenantId = appserviceTenantService::getCurrentTenantId();
            if ($tenantId) {
                $builder->where('tenant_id', $tenantId);
            }
        });
        
        // 自动设置租户ID
        static::creating(function ($model) {
            $tenantId = appserviceTenantService::getCurrentTenantId();
            if ($tenantId && !isset($model->tenant_id)) {
                $model->tenant_id = $tenantId;
            }
        });
    }
    
    // 移除租户作用域
    public function scopeWithoutTenant($query)
    {
        return $query->withoutGlobalScope('tenant');
    }
}

3. 租户服务类

<?php
namespace appservice;

class TenantService
{
    protected static $currentTenant = null;
    
    public static function setCurrentTenant($tenant)
    {
        self::$currentTenant = $tenant;
    }
    
    public static function getCurrentTenant()
    {
        return self::$currentTenant;
    }
    
    public static function getCurrentTenantId()
    {
        return self::$currentTenant ? self::$currentTenant->id : null;
    }
    
    // 创建新租户
    public static function createTenant($data)
    {
        $tenant = new appmodelTenant();
        $tenant->save($data);
        
        // 创建独立数据库
        if (config('tenant.database_strategy') === 'isolated') {
            self::createTenantDatabase($tenant);
        }
        
        return $tenant;
    }
    
    private static function createTenantDatabase($tenant)
    {
        $databaseName = 'tenant_' . $tenant->id . '_' . time();
        
        // 执行数据库创建和表结构初始化
        $sql = "CREATE DATABASE IF NOT EXISTS `{$databaseName}` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci";
        thinkfacadeDb::execute($sql);
        
        // 初始化表结构
        self::initializeTenantDatabase($databaseName);
        
        // 更新租户数据库名称
        $tenant->save(['database_name' => $databaseName]);
    }
}

4. 租户控制器示例

<?php
namespace appcontroller;

use appBaseController;
use appmodelUser;

class UserController extends BaseController
{
    use appmodeltraitTenantScope;
    
    public function index()
    {
        // 自动应用租户作用域
        $users = User::withSearch(['name', 'email'], [
            'name'  => $this->request->param('name'),
            'email' => $this->request->param('email')
        ])->paginate();
        
        return json([
            'code' => 200,
            'data' => $users
        ]);
    }
    
    public function create()
    {
        $data = $this->request->only(['name', 'email', 'password']);
        
        $user = new User();
        $user->save($data);
        
        return json([
            'code' => 200,
            'message' => '用户创建成功',
            'data' => $user
        ]);
    }
}

四、高级特性与优化

1. 多数据库连接管理

// database.php 配置
return [
    'default' => 'mysql',
    'connections' => [
        'mysql' => [
            'type'      => 'mysql',
            'hostname'  => '127.0.0.1',
            'database'  => 'main_system',
            'username'  => 'root',
            'password'  => '',
            'charset'   => 'utf8mb4',
        ],
        'tenant_template' => [
            'type'      => 'mysql',
            'hostname'  => '127.0.0.1',
            'username'  => 'root',
            'password'  => '',
            'charset'   => 'utf8mb4',
        ],
    ],
];

2. 租户数据备份与恢复

<?php
namespace appcommand;

use thinkconsoleCommand;
use thinkconsoleInput;
use thinkconsoleOutput;

class TenantBackup extends Command
{
    protected function configure()
    {
        $this->setName('tenant:backup')
             ->setDescription('备份租户数据');
    }
    
    protected function execute(Input $input, Output $output)
    {
        $tenants = appmodelTenant::select();
        
        foreach ($tenants as $tenant) {
            if ($tenant->database_name) {
                $this->backupTenantDatabase($tenant, $output);
            }
        }
        
        $output->writeln('所有租户数据备份完成');
    }
    
    private function backupTenantDatabase($tenant, $output)
    {
        $backupFile = runtime_path('backup') . "tenant_{$tenant->id}_" . date('YmdHis') . '.sql';
        
        $command = sprintf(
            'mysqldump -u%s -p%s %s > %s',
            config('database.connections.tenant_template.username'),
            config('database.connections.tenant_template.password'),
            $tenant->database_name,
            $backupFile
        );
        
        system($command, $result);
        
        if ($result === 0) {
            $output->writeln("租户 {$tenant->name} 备份成功: {$backupFile}");
        } else {
            $output->writeln("租户 {$tenant->name} 备份失败");
        }
    }
}

五、部署与运维

1. 环境配置

// .env 环境配置
APP_DEBUG = false
APP_TRACE = false

[ DATABASE ]
TYPE = mysql
HOSTNAME = 127.0.0.1
DATABASE = main_system
USERNAME = root
PASSWORD = your_password
HOSTPORT = 3306
CHARSET = utf8mb4

[ TENANT ]
DATABASE_STRATEGY = isolated  # isolated: 独立数据库, shared: 共享数据库
MAX_TENANTS_PER_SERVER = 1000
BACKUP_ENABLED = true

2. Nginx 多租户配置

server {
    listen 80;
    server_name ~^(?<subdomain>.+).main-domain.com$;
    
    root /path/to/your/project/public;
    index index.php index.html;
    
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
    
    location ~ .php$ {
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
        
        # 传递子域名信息
        fastcgi_param HTTP_X_TENANT_SUBDOMAIN $subdomain;
    }
}

3. 监控与日志

<?php
// 租户操作日志中间件
class TenantLog
{
    public function handle($request, Closure $next)
    {
        $response = $next($request);
        
        // 记录租户操作日志
        $tenantId = appserviceTenantService::getCurrentTenantId();
        if ($tenantId) {
            thinkfacadeLog::write([
                'tenant_id' => $tenantId,
                'url'       => $request->url(),
                'method'    => $request->method(),
                'ip'        => $request->ip(),
                'user_agent'=> $request->header('user-agent'),
                'time'      => date('Y-m-d H:i:s')
            ], 'tenant_operation');
        }
        
        return $response;
    }
}

ThinkPHP 6.0多租户SaaS系统架构设计与实现完整指南
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

淘吗网 thinkphp ThinkPHP 6.0多租户SaaS系统架构设计与实现完整指南 https://www.taomawang.com/server/thinkphp/1303.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务