ThinkPHP 8.x多租户SaaS系统架构设计与完整实现教程

2025-09-29 0 112

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

多租户SaaS系统架构概述

多租户架构是SaaS系统的核心设计模式,允许多个客户共享同一套应用程序实例,同时保持数据隔离和个性化配置。

三种多租户数据隔离方案

  • 独立数据库:每个租户拥有独立的数据库,安全性最高
  • 共享数据库独立Schema:同一数据库,不同数据表结构
  • 共享数据库共享Schema:同一数据库和表结构,通过tenant_id区分

系统架构设计

目录结构设计

app/
├── controller/
│   ├── Tenant.php          # 租户基类控制器
│   └── admin/
│       └── TenantManage.php # 租户管理
├── middleware/
│   └── TenantAuth.php      # 租户身份验证
├── service/
│   └── TenantService.php   # 租户业务逻辑
├── model/
│   ├── Tenant.php          # 租户模型
│   └── trait/
│       └── TenantScope.php # 租户数据范围
└── common.php              # 公共函数

数据库设计

-- 主数据库:系统管理表
CREATE TABLE `tenants` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `name` varchar(100) NOT NULL COMMENT '租户名称',
    `subdomain` varchar(50) UNIQUE NOT NULL COMMENT '子域名',
    `database_name` varchar(50) NOT NULL COMMENT '租户数据库名',
    `status` tinyint(1) DEFAULT 1 COMMENT '状态',
    `created_at` datetime DEFAULT NULL,
    `expires_at` datetime DEFAULT NULL COMMENT '过期时间',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 租户数据库:业务数据表
CREATE TABLE `tenant_users` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `tenant_id` int(11) NOT NULL,
    `name` varchar(50) NOT NULL,
    `email` varchar(100) NOT NULL,
    PRIMARY KEY (`id`),
    KEY `idx_tenant` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

核心功能实现

1. 租户识别中间件

<?php
namespace appmiddleware;

class TenantAuth
{
    public function handle($request, Closure $next)
    {
        // 通过子域名识别租户
        $subdomain = $this->getSubdomain($request->host());
        
        if (empty($subdomain) || $subdomain === 'www') {
            // 主域名访问,跳转到租户注册或管理页面
            return redirect('/admin/tenant');
        }
        
        // 查询租户信息
        $tenant = appmodelTenant::where('subdomain', $subdomain)
            ->where('status', 1)
            ->find();
            
        if (!$tenant) {
            throw new thinkexceptionHttpException(404, '租户不存在');
        }
        
        // 设置当前租户上下文
        appserviceTenantService::setCurrentTenant($tenant);
        
        // 切换数据库连接(如果是独立数据库模式)
        $this->switchDatabase($tenant);
        
        return $next($request);
    }
    
    private function getSubdomain($host)
    {
        $parts = explode('.', $host);
        if (count($parts) > 2) {
            return $parts[0];
        }
        return '';
    }
    
    private function switchDatabase($tenant)
    {
        $config = [
            'connections' => [
                'tenant_db' => [
                    'type' => 'mysql',
                    'hostname' => env('database.hostname'),
                    'database' => $tenant->database_name,
                    'username' => env('database.username'),
                    'password' => env('database.password'),
                    'charset' => 'utf8mb4',
                    'prefix' => '',
                ]
            ]
        ];
        
        thinkfacadeDb::config($config);
        thinkfacadeDb::connect('tenant_db');
    }
}

2. 租户服务类

<?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 getTenantId()
    {
        return self::$currentTenant ? self::$currentTenant->id : 0;
    }
    
    /**
     * 创建新租户
     */
    public function createTenant($data)
    {
        // 验证子域名唯一性
        $exists = appmodelTenant::where('subdomain', $data['subdomain'])->find();
        if ($exists) {
            throw new Exception('子域名已被占用');
        }
        
        // 生成数据库名
        $databaseName = 'tenant_' . $data['subdomain'] . '_' . time();
        
        // 创建租户记录
        $tenant = new appmodelTenant();
        $tenant->save([
            'name' => $data['name'],
            'subdomain' => $data['subdomain'],
            'database_name' => $databaseName,
            'created_at' => date('Y-m-d H:i:s'),
            'expires_at' => date('Y-m-d H:i:s', strtotime('+1 year'))
        ]);
        
        // 创建租户数据库
        $this->createTenantDatabase($databaseName);
        
        // 初始化租户数据表
        $this->initTenantTables($databaseName);
        
        return $tenant;
    }
    
    private function createTenantDatabase($databaseName)
    {
        $sql = "CREATE DATABASE IF NOT EXISTS `{$databaseName}` 
                DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci";
        thinkfacadeDb::execute($sql);
    }
    
    private function initTenantTables($databaseName)
    {
        // 切换到租户数据库执行建表语句
        $originalConfig = thinkfacadeDb::getConfig();
        
        $config = array_merge($originalConfig, ['database' => $databaseName]);
        thinkfacadeDb::connect($config);
        
        // 执行租户业务表创建
        $tableScript = file_get_contents(app()->getRootPath() . 'database/tenant_tables.sql');
        thinkfacadeDb::execute($tableScript);
        
        // 恢复默认数据库连接
        thinkfacadeDb::connect($originalConfig);
    }
}

3. 租户数据范围Traits

<?php
namespace appmodeltrait;

trait TenantScope
{
    /**
     * 自动添加租户ID过滤
     */
    public static function onBeforeInsert($model)
    {
        $tenantId = appserviceTenantService::getTenantId();
        if ($tenantId) {
            $model->tenant_id = $tenantId;
        }
    }
    
    /**
     * 自动限制租户数据范围
     */
    public function scopeTenant($query)
    {
        $tenantId = appserviceTenantService::getTenantId();
        if ($tenantId) {
            $query->where('tenant_id', $tenantId);
        }
        return $query;
    }
}

// 在模型中使用
namespace appmodel;

use thinkModel;
use appmodeltraitTenantScope;

class TenantUser extends Model
{
    use TenantScope;
    
    // 自动写入时间戳
    protected $autoWriteTimestamp = true;
    
    // 定义租户ID字段
    protected $tenantIdField = 'tenant_id';
    
    /**
     * 查询租户用户列表(自动应用租户范围)
     */
    public function getTenantUsers()
    {
        return $this->tenant()->select();
    }
}

高级特性实现

1. 多数据库连接管理

<?php
namespace appservice;

class DatabaseManager
{
    /**
     * 动态切换数据库连接
     */
    public static function switchToTenantDatabase($tenant)
    {
        $config = [
            'type' => 'mysql',
            'hostname' => env('database.hostname'),
            'database' => $tenant->database_name,
            'username' => env('database.username'),
            'password' => env('database.password'),
            'charset' => 'utf8mb4',
            'prefix' => '',
        ];
        
        // 注册新的数据库连接
        thinkfacadeDb::connect($config, 'tenant_connection');
        
        return 'tenant_connection';
    }
    
    /**
     * 跨数据库查询(主库与租户库)
     */
    public static function crossDatabaseQuery()
    {
        // 主数据库查询
        $tenants = thinkfacadeDb::connect('default')
            ->name('tenants')
            ->where('status', 1)
            ->select();
            
        $results = [];
        foreach ($tenants as $tenant) {
            // 切换到租户数据库
            $connectionName = self::switchToTenantDatabase($tenant);
            
            // 租户数据库查询
            $userCount = thinkfacadeDb::connect($connectionName)
                ->name('tenant_users')
                ->count();
                
            $results[] = [
                'tenant' => $tenant->name,
                'user_count' => $userCount
            ];
        }
        
        return $results;
    }
}

2. 租户自定义配置

<?php
namespace appservice;

class TenantConfig
{
    /**
     * 获取租户自定义配置
     */
    public static function getConfig($key = null, $default = null)
    {
        $tenantId = TenantService::getTenantId();
        if (!$tenantId) {
            return $default;
        }
        
        // 从缓存或数据库获取配置
        $cacheKey = "tenant_config_{$tenantId}";
        $config = cache($cacheKey);
        
        if (!$config) {
            $config = thinkfacadeDb::connect('default')
                ->name('tenant_configs')
                ->where('tenant_id', $tenantId)
                ->column('value', 'key');
            cache($cacheKey, $config, 3600);
        }
        
        if ($key) {
            return $config[$key] ?? $default;
        }
        
        return $config;
    }
    
    /**
     * 设置租户配置
     */
    public static function setConfig($key, $value)
    {
        $tenantId = TenantService::getTenantId();
        if (!$tenantId) {
            return false;
        }
        
        $data = [
            'tenant_id' => $tenantId,
            'key' => $key,
            'value' => $value,
            'updated_at' => date('Y-m-d H:i:s')
        ];
        
        // 更新或插入配置
        thinkfacadeDb::connect('default')
            ->name('tenant_configs')
            ->insert($data, true);
            
        // 清除缓存
        cache("tenant_config_{$tenantId}", null);
        
        return true;
    }
}

部署与运维方案

Nginx子域名配置

server {
    listen 80;
    server_name ~^(?<subdomain>.+).yourdomain.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 unix:/var/run/php/php8.1-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
        
        # 传递子域名信息
        fastcgi_param HTTP_X_TENANT_SUBDOMAIN $subdomain;
    }
}

数据库备份策略

<?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::where('status', 1)->select();
        
        foreach ($tenants as $tenant) {
            $this->backupTenantDatabase($tenant, $output);
        }
        
        $output->writeln('所有租户数据库备份完成');
    }
    
    private function backupTenantDatabase($tenant, $output)
    {
        $backupDir = runtime_path('backup/' . date('Y-m-d'));
        if (!is_dir($backupDir)) {
            mkdir($backupDir, 0755, true);
        }
        
        $backupFile = $backupDir . '/tenant_' . $tenant->subdomain . '.sql';
        
        $command = sprintf(
            'mysqldump -h%s -u%s -p%s %s > %s',
            env('database.hostname'),
            env('database.username'),
            env('database.password'),
            $tenant->database_name,
            $backupFile
        );
        
        exec($command, $result, $status);
        
        if ($status === 0) {
            $output->writeln("租户 {$tenant->name} 备份成功: {$backupFile}");
        } else {
            $output->writeln("租户 {$tenant->name} 备份失败");
        }
    }
}

document.addEventListener(‘DOMContentLoaded’, function() {
const codeBlocks = document.querySelectorAll(‘pre code’);

codeBlocks.forEach(block => {
block.addEventListener(‘click’, function() {
const textArea = document.createElement(‘textarea’);
textArea.value = this.textContent;
document.body.appendChild(textArea);
textArea.select();

try {
document.execCommand(‘copy’);
console.log(‘代码已复制到剪贴板’);
} catch (err) {
console.error(‘复制失败:’, err);
}

document.body.removeChild(textArea);
});

block.title = ‘点击复制代码’;
});
});

ThinkPHP 8.x多租户SaaS系统架构设计与完整实现教程
收藏 (0) 打赏

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

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

淘吗网 thinkphp ThinkPHP 8.x多租户SaaS系统架构设计与完整实现教程 https://www.taomawang.com/server/thinkphp/1136.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

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

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