ThinkPHP 6.x 多租户SaaS系统架构实战:从零构建企业级应用 | PHP框架开发教程

2026-02-17 0 623
免费资源下载

从零构建企业级多租户应用,掌握数据库隔离与数据权限的核心技术

多租户架构设计模式解析

SaaS系统开发中,多租户架构是核心技术挑战。ThinkPHP 6.x提供了灵活的扩展机制,支持三种主流的多租户实现方案。

1. 独立数据库模式

  • 每个租户拥有独立数据库
  • 数据隔离级别最高
  • 适合大型企业客户
  • 运维成本相对较高

2. 共享数据库独立Schema

  • 同一数据库不同数据表
  • 平衡隔离与成本
  • 适合中型SaaS应用
  • 需要动态表名支持

3. 共享数据库共享表

  • 通过tenant_id字段区分
  • 成本最低实现最简单
  • 适合初创SaaS项目
  • 数据隔离依赖程序逻辑

实战案例:共享数据库共享表模式实现

我们将采用第三种方案,通过中间件和模型基类实现数据自动隔离,这是最适合快速启动项目的方案。

Step 1: 数据库设计

-- 租户表
CREATE TABLE `tenants` (
    `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
    `tenant_code` varchar(50) NOT NULL COMMENT '租户标识码',
    `name` varchar(100) NOT NULL COMMENT '租户名称',
    `status` tinyint(1) DEFAULT 1 COMMENT '状态:1-正常 0-禁用',
    `expire_time` datetime DEFAULT NULL COMMENT '过期时间',
    `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
    `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_tenant_code` (`tenant_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 用户表(包含tenant_id字段)
CREATE TABLE `users` (
    `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
    `tenant_id` int(11) UNSIGNED NOT NULL COMMENT '租户ID',
    `username` varchar(50) NOT NULL COMMENT '用户名',
    `email` varchar(100) NOT NULL COMMENT '邮箱',
    `password` varchar(255) NOT NULL COMMENT '密码',
    `is_admin` tinyint(1) DEFAULT 0 COMMENT '是否管理员',
    `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
    `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY `idx_tenant` (`tenant_id`),
    UNIQUE KEY `uk_tenant_username` (`tenant_id`, `username`),
    UNIQUE KEY `uk_tenant_email` (`tenant_id`, `email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 其他业务表都需要包含tenant_id字段
CREATE TABLE `products` (
    `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
    `tenant_id` int(11) UNSIGNED NOT NULL COMMENT '租户ID',
    `name` varchar(200) NOT NULL COMMENT '产品名称',
    `price` decimal(10,2) NOT NULL COMMENT '价格',
    `stock` int(11) DEFAULT 0 COMMENT '库存',
    `status` tinyint(1) DEFAULT 1 COMMENT '状态',
    `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
    `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY `idx_tenant` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Step 2: 创建租户识别中间件

<?php
// app/middleware/TenantIdentify.php
namespace appmiddleware;

use thinkfacadeDb;
use thinkfacadeSession;

class TenantIdentify
{
    public function handle($request, Closure $next)
    {
        // 从子域名识别租户(如:company1.saasapp.com)
        $host = $request->host();
        $subdomain = $this->extractSubdomain($host);
        
        // 或者从请求头识别(适合移动端API)
        $tenantCode = $request->header('X-Tenant-Code') 
                    ?: $request->param('tenant_code') 
                    ?: $subdomain;
        
        if (!$tenantCode) {
            // 跳转到主域名或显示租户选择页面
            return redirect('/select-tenant');
        }
        
        // 查询租户信息
        $tenant = Db::name('tenants')
            ->where('tenant_code', $tenantCode)
            ->where('status', 1)
            ->find();
        
        if (!$tenant) {
            return json(['code' => 404, 'msg' => '租户不存在或已禁用']);
        }
        
        // 检查租户是否过期
        if ($tenant['expire_time'] && strtotime($tenant['expire_time']) 

Step 3: 创建多租户模型基类

<?php
// app/model/TenantModel.php
namespace appmodel;

use thinkModel;
use thinkfacadeSession;

abstract class TenantModel extends Model
{
    // 自动写入租户ID
    protected $autoWriteTenantId = true;
    
    // 租户ID字段名
    protected $tenantField = 'tenant_id';
    
    protected static function onBeforeInsert(Model $model)
    {
        if ($model->autoWriteTenantId) {
            $tenantId = Session::get('tenant_id');
            if ($tenantId && empty($model->getData($model->tenantField))) {
                $model->set($model->tenantField, $tenantId);
            }
        }
    }
    
    protected static function onBeforeUpdate(Model $model)
    {
        // 更新时确保不修改租户ID
        if ($model->autoWriteTenantId) {
            $model->readonly($model->tenantField);
        }
    }
    
    protected static function onBeforeSelect(Model $model)
    {
        // 自动添加租户查询条件
        if ($model->autoWriteTenantId) {
            $tenantId = Session::get('tenant_id');
            if ($tenantId) {
                $model->where($model->getTable() . '.' . $model->tenantField, $tenantId);
            }
        }
    }
    
    /**
     * 获取当前租户的所有数据(管理员权限)
     */
    public function scopeTenant($query, $tenantId = null)
    {
        $tenantId = $tenantId ?: Session::get('tenant_id');
        return $query->where($this->tenantField, $tenantId);
    }
    
    /**
     * 临时关闭租户过滤(用于超级管理员)
     */
    public function withoutTenantScope()
    {
        $this->autoWriteTenantId = false;
        return $this;
    }
}

Step 4: 业务模型实现

<?php
// app/model/Product.php
namespace appmodel;

class Product extends TenantModel
{
    // 定义数据表名
    protected $table = 'products';
    
    // 自动写入时间戳
    protected $autoWriteTimestamp = true;
    protected $createTime = 'created_at';
    protected $updateTime = 'updated_at';
    
    // 定义字段类型
    protected $type = [
        'price' => 'float',
        'stock' => 'integer',
        'status' => 'integer'
    ];
    
    // 搜索器:按名称搜索
    public function searchNameAttr($query, $value)
    {
        return $query->whereLike('name', '%' . $value . '%');
    }
    
    // 搜索器:按价格范围搜索
    public function searchPriceRangeAttr($query, $value)
    {
        if (isset($value[0])) {
            $query->where('price', '>=', $value[0]);
        }
        if (isset($value[1])) {
            $query->where('price', ' Request::param('name'),
            'priceRange' => [Request::param('min_price'), Request::param('max_price')]
        ])
        ->order('id', 'desc')
        ->paginate([
            'page' => $page,
            'list_rows' => $size
        ]);
        
        return json([
            'code' => 200,
            'data' => $products->items(),
            'total' => $products->total(),
            'current_page' => $products->currentPage()
        ]);
    }
    
    // 创建产品(自动添加tenant_id)
    public function create()
    {
        $data = Request::only(['name', 'price', 'stock', 'status']);
        
        $product = new Product();
        $result = $product->save($data);
        
        if ($result) {
            return json(['code' => 200, 'msg' => '创建成功', 'data' => $product]);
        } else {
            return json(['code' => 500, 'msg' => '创建失败']);
        }
    }
    
    // 更新产品(确保只能更新当前租户的数据)
    public function update($id)
    {
        $product = Product::find($id);
        if (!$product) {
            return json(['code' => 404, 'msg' => '产品不存在']);
        }
        
        $data = Request::only(['name', 'price', 'stock', 'status']);
        $result = $product->save($data);
        
        if ($result) {
            return json(['code' => 200, 'msg' => '更新成功']);
        } else {
            return json(['code' => 500, 'msg' => '更新失败']);
        }
    }
    
    // 删除产品
    public function delete($id)
    {
        $product = Product::find($id);
        if (!$product) {
            return json(['code' => 404, 'msg' => '产品不存在']);
        }
        
        // 软删除(如果模型支持)
        $result = $product->delete();
        
        if ($result) {
            return json(['code' => 200, 'msg' => '删除成功']);
        } else {
            return json(['code' => 500, 'msg' => '删除失败']);
        }
    }
}

高级功能:租户数据隔离与权限控制

1. 动态数据库连接

// 根据租户动态切换数据库配置
$tenant = $request->tenant;
if ($tenant['database_type'] === 'independent') {
    $config = [
        'type' => 'mysql',
        'hostname' => $tenant['db_host'],
        'database' => $tenant['db_name'],
        'username' => $tenant['db_user'],
        'password' => $tenant['db_pass'],
        'hostport' => $tenant['db_port']
    ];
    Db::connect($config, 'tenant_' . $tenant['id']);
}

2. Redis多租户隔离

// 使用不同的Redis数据库或前缀
$tenantId = Session::get('tenant_id');
$redis = new Redis();
$redis->select($tenantId); // 选择不同的DB

// 或者使用前缀
$cacheKey = 'tenant:' . $tenantId . ':user:' . $userId;
$redis->set($cacheKey, $userData);

3. 文件存储隔离方案

// 配置文件存储路径
// config/filesystem.php
return [
    'default' => 'local',
    'disks' => [
        'local' => [
            'type' => 'local',
            'root' => app()->getRootPath() . 'storage',
        ],
        'tenant' => [
            'type' => 'local',
            'root' => function() {
                $tenantId = Session::get('tenant_id');
                return app()->getRootPath() . 'storage/tenants/' . $tenantId;
            }
        ],
    ]
];

// 使用租户隔离的文件存储
$file = request()->file('image');
$saveName = thinkfacadeFilesystem::disk('tenant')
    ->putFile('uploads', $file);

性能优化与监控

数据库优化

  • 为tenant_id字段建立索引
  • 使用数据库连接池
  • 定期清理过期租户数据
  • 分表策略应对大数据量

缓存策略

  • 租户级缓存前缀隔离
  • 热点数据预加载
  • 缓存雪崩防护机制
  • 多级缓存架构

监控告警

  • 租户API调用统计
  • 数据库连接数监控
  • 慢查询日志分析
  • 自动扩缩容机制

部署与运维指南

Nginx配置示例

server {
    listen 80;
    server_name ~^(?<subdomain>.+).saasapp.com$;
    
    root /var/www/saasapp/public;
    index index.php;
    
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
    
    location ~ .php$ {
        fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param HTTP_HOST $host;
        include fastcgi_params;
        
        # 传递子域名到PHP
        fastcgi_param X_TENANT_CODE $subdomain;
    }
}

Docker Compose配置

version: '3.8'
services:
  app:
    build: .
    volumes:
      - ./:/var/www/html
    environment:
      - APP_ENV=production
      - APP_DEBUG=false
      - TENANT_MODE=shared
  
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./docker/nginx.conf:/etc/nginx/nginx.conf
      - ./public:/var/www/html/public
  
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root_password
      MYSQL_DATABASE: saas_main
    volumes:
      - mysql_data:/var/lib/mysql
  
  redis:
    image: redis:alpine
    command: redis-server --appendonly yes
  
volumes:
  mysql_data:

安全注意事项

  • SQL注入防护:确保所有查询都使用参数绑定,ThinkPHP的ORM已提供基础防护
  • 跨租户数据泄露:严格验证每个数据操作的租户权限,避免查询条件被绕过
  • 会话隔离:不同租户使用不同的会话存储,避免会话数据混淆
  • 文件上传安全:限制文件类型和大小,使用租户隔离的存储路径
  • API限流:为每个租户设置独立的API调用频率限制
  • 数据备份:制定租户数据的定期备份和恢复策略

ThinkPHP 6.x 多租户SaaS系统架构实战:从零构建企业级应用 | PHP框架开发教程
收藏 (0) 打赏

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

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

淘吗网 thinkphp ThinkPHP 6.x 多租户SaaS系统架构实战:从零构建企业级应用 | PHP框架开发教程 https://www.taomawang.com/server/thinkphp/1610.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

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